BASE開発チームブログ

Eコマースプラットフォーム「BASE」( https://thebase.in )の開発チームによるブログです。開発メンバー積極募集中! https://www.wantedly.com/companies/base/projects

stylelintとBackstopJSで安全にcssを書ける環境を作った

20180605164447

こんにちは。BASE で Design Group に所属している三佐和です。主に ネットショップ作成サービス「BASE」 のフロントエンドを担当しています。

背景

BASE のデザインチームはここ最近で人数が急激に増え、活動が活発になってきており、その中のプロジェクトの一つとして、現在スタイルガイドの刷新に取り組んでいます。

しかし、人数が増えていく一方で、コーディングのルールの統一をコードレビューや個人の裁量に任せていたり、マークアップからリリースするまでに時間がかかってしまうことが問題になってきていました。 そこで、新しいスタイルガイドでは、デザイナーやエンジニアの作業工数を短縮し、より効率よく開発を進めるために、コーディングルールの整備とリグレッションテストを導入することにしました!

やったこと

  1. stylelint を使ってコーディングルールを管理
  2. BackstopJS でテストを行うことでデグレを防ぐ

前提として、 nodejs や yarn などの一般的なフロントエンド開発環境が整っているものとします。

stylelintを導入

stylelint を使って、これまで個人の裁量で保たれていたコーディング規約への準拠を機械的に行えるようにします。

導入

  • stylelintをインストール
  • その他必要なプラグイン等をインストール
  • yarn を使ってインストールします。

    $ yarn add -D stylelint stylelint-scss stylelint-order stylelint-config-standard prettier-stylelint
    

    設定

    既存のコーディングルールに合わせて、.stylelintrc を設定していきます。 BASE の場合はプロパティをアルファベット順にソートしたかったので、stylelint-order を使用したりしています。(私はいままで abc の歌を歌いながら順番にプロパティを並べていました........)

    // .stylelintrc
    {
      "parser": ["css"],
      "plugins": ["stylelint-scss", "stylelint-order"],
      "extends": ["stylelint-config-standard", "./node_modules/prettier-stylelint/config.js"],
      "rules": {
        "indentation": 4, // インデント
        "order/order": [
            "custom-properties",
            "declarations"
        ], // アルファベット順でソートする
        "order/properties-alphabetical-order": true, // アルファベット順でソートする
        "length-zero-no-unit": true, // 値が「0」なら単位を省略する
        "number-leading-zero": "always", // 小数点の頭の「0」は省略する
        "color-hex-length": "short", // HEX形式のカラーコードは3文字で表記する
        "shorthand-property-no-redundant-values": true // ショートハンドでプロパティを書く
      }
    }
    

    設定ができたら、package.jsonscripts にコマンドを追加して、 yarn lint-sass で実行できるようにします。

    // package.json
    // ...
    "scripts": {
       "lint-sass": "prettier-stylelint --quiet --write src/sass/**/*"
    }
    // ...
    

    実際の使用方法

    まずは思うがままに sass(scss) を記述します。

    .btn--submit {
        color: $white;
         background-color: $green;
     border: none;
    }
    

    めちゃくちゃですね!でも大丈夫です、lint を実行すると...

    $ yarn lint-sass
    

    こうなります。

    .btn--submit {
      background-color: $green;
      border: none;
      color: $white;
    }
    

    完璧ですね!もう歌は歌わなくても大丈夫になりました!

    結果

    常にコーディングルールを意識する必要がなくなったので、マークアップのスピードも上がりました!

    2. BackstopJSを導入

    BackstopJS を使って、戻りが発生しないよう、動作チェックする仕組みを作ります。

    導入

    • BackstopJSをインストール

    stylelintと同様に、yarn を使ってインストールします。

    $ yarn add -D backstopjs
    
    • backstop.json にオプションを設定し、スナップショットを撮る
    • スナップショットを使ってテストする

    設定

    backstop.json に必要なオプションを設定していきます。

    // backstop.json
    {
      "viewports": [
        {
          "label": "sp",
          "width": 320,
          "height": 480
        },
        {
          "label": "pc",
          "width": 1024,
          "height": 768
        }
      ],
      "scenarios": [
        {
          "label": "reference", // スナップショットの名前
          "url": "テストするURL",
          "hideSelectors": [],
          "removeSelectors": [],
          "selectors": [
            "#test-sandbox" // スナップショットを撮影する部分
          ],
          "readyEvent": null,
          "delay": 500,
          "misMatchThreshold" : 0.1,
          "onBeforeScript": "",
          "onReadyScript": ""
        },
        {
          "label": "test", // スナップショットの名前
          "url": "テストするURL",
          "hideSelectors": [],
          "removeSelectors": [],
          "selectors": [
            "#test-sandbox" // スナップショットを撮影する部分
          ],
          "readyEvent": null,
          "delay": 500,
          "misMatchThreshold" : 0.1,
          "onBeforeScript": "",
          "onReadyScript": ""
        }
    
      ],
      "paths": {
        "bitmaps_reference": "./backstop_data/bitmaps_reference",
        "bitmaps_test": "./backstop_data/bitmaps_test",
        "compare_data": "./backstop_data/bitmaps_test/compare.json",
        "casper_scripts": "./backstop_data/casper_scripts"
      },
      "engine": "phantomjs",
      "report": ["CLI", "browser"],
      "cliExitOnFail": false,
      "casperFlags": [],
      "debug": false,
      "port": 3001
    }
    

    設定ができたら gulpfile にタスクを追加します。

    // gulpfile.coffee
    // ...
    exports.backstopref = () =>
      connect.server({root: './build', livereload: true})
      backstopjs('reference').then () ->
          connect.serverClose()
    
    exports.backstop = () =>
      connect.server({root: './build', livereload: true})
      backstopjs('test').then () ->
          connect.serverClose()
    // ...
    
    
    

    そして、package.jsonscripts にコマンドを追加して、 yarn backstop-ref で参照用画像の作成、 yarn backstop-test でテストが実行できるようにします。

    // package.json
    // ...
    "scripts": {
        "backstop-ref": "gulp backstopref",
        "backstop-test": "gulp backstop",
    }
    // ...
    

    実際の使用方法

    はじめに、テストの元データになる参照用の画像を作ります。

    $ yarn backstop-ref
    

    20180605170855

    BackstopJS は、この画像をもとに比較テストを行ってくれるので、予期せぬ変更を行ってしまった場合にテストが失敗し、それに気づくことができます。

    試しに、このボタンの border-radius8px から 0px へ変更してみましょう。 そしておもむろにテスト実行用のコマンドを叩きます。

    $ yarn backstop-test
    // ...
          compare | ERROR { requireSameDimensions: false, size: isDifferent, content: 0.32%, threshold: 0.1% }: reference bbq_reference_0_test-sandbox_0_sp.png
          compare | ERROR { requireSameDimensions: false, size: isDifferent, content: 0.32%, threshold: 0.1% }: test bbq_test_0_test-sandbox_0_sp.png
          compare | OK: reference bbq_reference_0_test-sandbox_1_pc.png
          compare | OK: test bbq_test_0_test-sandbox_1_pc.png
           report | Test completed...
           report | 2 Passed
           report | 2 Failed
           report | Writing jUnit Report
           report | Writing browser report
           report | jUnit report written to: ~~/backstop_data/ci_report/xunit.xml
           report | Resources copied
           report | Copied configuration to: ~~/backstop_data/html_report/config.js
          COMMAND | Executing core for `openReport`
       openReport | Opening report.
           report | *** Mismatch errors found ***
          COMMAND | Command `report` ended with an error after [0.51s]
          COMMAND | Error: Mismatch errors found.
                        at ~~/backstopjs/core/command/report.js:113:17
                        at <anonymous>:null:null
    
          COMMAND | Command `test` ended with an error after [5.371s]
          COMMAND | Error: Mismatch errors found.
                        at ~~/backstopjs/core/command/report.js:113:17
                        at <anonymous>:null:null
    
    [15:02:59] 'backstop' errored after 5.38 s
    

    差分が発生したので、テストが失敗し、レポート用の HTML が出力されました。

    20180605164934

    これを開いてみると、差分のある箇所がピンク色になっています!分かりやすいですね!

    結果

    作業を開始する前にスナップショットを作成しておくことで、行った作業でのデグレに早く気づいたり、防ぐことできるようになりました。

    まとめ

    今回、stylelintやBackstopJSを利用したことで、実装者の負担が以前よりも軽くなりました!

    実装者が増えても差分が生まれにくく、デザインの一貫性を保つことができる仕組みを考え、引き続きスタイルガイドを作成していきたいと思います。また同時に、それらをメンテナンスしやすい環境も整えていきたいと思います!

    Go言語勉強会を始めたら学習のペースメーカーになった話

    f:id:khigashigashi:20180521090446j:plain

    こんにちは!BASE Product Division サーバーサイドエンジニアの東口です。主にEコマースプラットフォーム「BASE」の決済領域の開発をしています。本ブログでは、PHPerKaigi 2018での登壇記事等も書いています。

    devblog.thebase.in

    BASEのサーバーサイドの多くはPHPで書かれているので普段触る機会の多い言語はPHPなのですが、先月からGoを書きたい有志が集まって定期的にGo言語勉強会を行っています。

    きっかけ

    初めたきっかけは2018年4月15日に開催されたGo Conference 2018 Springでした。スポンサー企業のラインナップや実際に会場の熱気に触れて、改めてGo言語の勢いや実用性を強く再認識しました。それまでGoを書きたいと思っていたこともありカンファレンスに参加していた同僚と勢いで始めることを決めました。

    やり方

    対象書籍として、『Goならわかるシステムプログラミング』を進めていくことにしました。この書籍を選んだ理由は、Goという言語自体の書き方ではなくGoを使って目的のあるコードを書ける点でした。また、個人的にPHPを触ってると意識しにくい低レイヤの仕組みについて知っておきたいという意図もありました。

    f:id:khigashigashi:20180522163147j:plain

    Go Conference 2018 Springの翌週から毎週1回2時間定期的に時間を取って実施しています。

    集まってやるということで発表の時間を作ろうかという検討もありましたが、身内でやる分にはまずは各自好きなペースでもくもく進めていく方法で進めることにしています。

    f:id:khigashigashi:20180522163303j:plain

    また、読書会中の会話や詰まったときの内容をGo言語について話すslackの #golangチャネルにて話しています。ここで各自詰まったところについて話していたりしているので後続の人がそこを見て参考にできるという利点が有りました。

    良かったこと

    学習のペースメーカーになる

    定期的に時間をとることで途中で止まりがちな本の写経をする習慣ができ、週一回Goを触るペースメーカー的な存在になっています。

    議論ができる

    全員が同じ書籍をベースにやっているので、「このページで使われている関数の内部実装がどうなっているか」等、一人で進めているとスルーして先に進めがちな箇所について疑問を深掘りする機会になっています。

    Goに触れた経験のあるエンジニアが増えた

    本読書会を期に、「実はGoやりたかった」というエンジニアがGoを書き始めました。

    元から社内slackに#golangチャネルというGo言語について話すチャネルがあったのですが参加者が7人くらいで更新頻度もあまり高くありませんでした。

    しかし、読書会を通じてGo言語に触れたエンジニアが増え#golangチャネルに人が増えGoの話題について話す機会が増えました。Go言語のイベントの共有や「MySQLドライバをGoで書いてみたら面白そう」といった、こういうチャレンジをしたら面白そうと言った会話がslackでされています。

    まとめ

    プロダクションでGoを採用している箇所はスポット部分に限られるので、この読書会を通じてGoをプロダクション環境や趣味のプロダクトに適用するきっかけになればと思っています。

    読書会やもくもく会といった勉強会を始める際どういう風に運営するかみたいなことを考えて止まってしまうかもしれないですが、やってから考えるくらいの気持ちで始めてみてはいかがでしょうか?

    Embedded Frameworkを導入して、iOS アプリのビルドを爆速にした話

    f:id:yuhei_kagaya:20180509103406j:plain

    iOSエンジニアの大木です。

    日々の開発で、ちょっとした微修正でメソッドを追加・削除すると、差分コンパイルが効かずビルド10分待ちとなり、開発効率の低下が問題となっていました。それを解決するためEmbedded Frameworkを導入したところ、差分ビルドが成功し1-2分になったというお話です。

    私が入社したのは、2017年の2月でした。そして、アプリはもともとObjective-Cで書かれていて、Swiftコードの占める割合は10%以下だったようです。それから1年近くで、90%以上Swiftコードに置き換えました。

    その時点で、ビルドにかかる時間の長さが目立つようになり、ちょっとした不具合の調査や微調整しているだけなのに10分近く待たなければいけないケースが目立つようになってきました。

    問題としては以下の2つがありました。

    • 1つのメソッドを追加しただけなのに、何故か10分近くかかるため、微調整するのに不向き
    • タイプミスしたので一旦途中キャンセルして修正し再ビルドしたが、差分ビルドが効かず10分待ち

    前提

    日々の開発で行うDebugビルドを対象としています。そのため、AdHoc配信などでビルドする場合のケース(Archive)はCircle CIでおこなっているため特に対象にしていません。差分ビルドが効くようにして開発効率を改善していきます。

    環境

    • MacBook Pro (15-inch, 2016)
    • macOS High Sierra(10.13.4)
    • Xcode 9.2
    • Swift 4.0
    • Swift:Objective-Cの比率は、9:1
      • ビルド対象のSwiftファイルは約850ファイル
    • 一般的なビルド設定やSwift-Objective-Cのブリッジングに対する改善はすでに行なっていた
    • New build systemを使っている

    改善の流れ

    1. ビルド時間計測
    2. ビルドが遅くなっている原因コードを探して修正する
    3. これ以上速く出来ない?
    4. コードをモジュール単位に分割
    5. 差分ビルドが効くようになり、開発効率改善!

    ビルド時間計測

    パイク: Cプログラミングに関する覚え書き にもある通り、推測するのではなく、どこで時間がかかっているか計測してみます。

    XcodeのBuildSettingsの OTHER_SWIFT_FLAGS-Xfrontend -debug-time-function-bodies を追加すると、閾値以上のビルド時間がかかっているメソッドを、Xcode上でWarningレベルで報告してくれます。

    ちなみにBASEアプリでは、App Extensionsを使用していることもあり、設定の抜け漏れを防ぐため、XcodeのGUI上ではなく、xcconfigファイルで設定しています。

    // Debug.xcconfig
    
    #include "Shared.xcconfig"
    #include "DebugPlugins.xcconfig"
    
    SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEBUG;
    // 計測用フラグ
    OTHER_SWIFT_FLAGS = -enable-bridging-pch -Xfrontend -debug-time-function-bodies -Xfrontend -debug-time-compilation -Xfrontend -warn-long-function-bodies=200 -Xfrontend -warn-long-expression-type-checking=200;
    GCC_PREPROCESSOR_DEFINITIONS = $(inherited) DEBUG=1;
    

    計測結果が含まれるビルドログはXcode上で全ては見えないのと、出力されたファイルを読み解く時間が惜しいという場合は、BuildTimeAnalyzer-for-Xcode を使うとよいでしょう。

    ビルドが遅くなっている原因コードを探して修正する

    さて計測結果を眺めてみたところ、注文内容と支払い情報を送信するためのHTTPBodyの生成structを返す処理で10秒前後ビルドにかかっていました。 その処理は、Structに (String, Any) のタプルの配列で必要なパラメーターをセットしているのですが、型情報がわからないままなため時間がかかっていたようです。

    そこで、一旦変数に格納することによって、型推論が効くようになり、ビルド時間も大幅に改善しました。

    diff --git a/CartClient.swift b/CartClient.swift
    index 9e2b4c060..93a882f19 100644
    --- a/CartClient.swift
    +++ b/CartClient.swift
    @@ -42,6 +42,11 @@ class CartClient: APIClient {
             case .checkout(paymentType: let `type`, payment: let payment, cart: let cart):
                 let orderRequestInfo = cart.orderRequestInfo
    +            // 一旦変数に格納して型情報を用意する
    +            // 住所とメールアドレスは必須なのでforce unwrappingで取り出しても問題ないはず
    +            let familyName = payment.shippingContact!.name?.familyName ?? ""
    +            let givenName = payment.shippingContact!.name?.givenName ?? ""
    +            let tel = payment.shippingContact!.phoneNumber?.stringValue ?? ""
                 return Parameters([
                     ("payment", type.paymentMethod),
    @@ -49,14 +54,14 @@ class CartClient: APIClient {
                     ("item_id", orderRequestInfo.itemId),
                     ("shop_id", orderRequestInfo.shopId),
                     ("variation", orderRequestInfo.variationId),
                     ("amount", orderRequestInfo.amount),
    -                ("last_name", payment.shippingContact!.name?.familyName ?? ""),
    -                ("first_name", payment.shippingContact!.name?.givenName ?? ""),
    +                ("last_name", familyName),
    +                ("first_name", givenName),
                     ("postal_code", payment.shippingContact!.postalAddress!.postalCode),
                     ("state", payment.shippingContact!.postalAddress!.state),
                     ("address", payment.shippingContact!.postalAddress!.city),
    -                ("address2", payment.shippingContact!.postalAddress?.street ?? ""),
    -                ("tel", payment.shippingContact!.phoneNumber?.stringValue ?? ""),
    -                ("mail_address", payment.shippingContact!.emailAddress ?? ""),
    +                ("address2", payment.shippingContact!.postalAddress!.street),
    +                ("tel", tel),
    +                ("mail_address", payment.shippingContact!.emailAddress!),
                     ("version", appVersion())])
             default:
                 break
    

    改善結果は以下の通りです。

    コンパイル時間 内容
    9323.63ms 元のコード
    265.4ms(-97%) 追加する値を一旦変数に保存(Optionalのまま)
    16.6ms(-99.8%) さらにNonOptionalにして保存

    他にも修正していくらか改善しましたが、劇的な改善には至りませんでした。

    • if boolValue = true { のような比較をやめて if boolValue { に変更して中身の判定を行う
    • 条件に一致したコレクションの要素を取得するメソッド filter() を使うだけで条件を満たせるのに compactMap() 使って余計にビルド時間がかかっていた。

    100ms以内に収まり喜んだのもつかの間、再度ビルドすると200ms以上時間がかかってしまう問題もあり、これ以上は誤差でしかないと考え修正を打ち切りました。

    これ以上速く出来ない?

    Swiftコードコンパイル時間短縮も大事なのですが、最初に述べた 差分ビルドが効くようにして開発効率を改善 達成するのが第一の目標です。

    考えてみると、全てのソースファイルをアプリケーションターゲットでビルドするのは無駄です。APIからデータを取得するコードやデータモデルなどは、最初に定義してしまえばほとんど修正することはありません。

    モジュールとして切り出せば、関係ないコードのビルドはFramework側で行うことになり、ビルド時間の大幅な改善が見込めそうです。

    iOSには、Cocoa Touch Frameworkとしてコードを共有する仕組みがあります。さらに、Embedded Frameworkという仕組みを使ってXcodeのワークスペースに直接組み込むこともできます。

    コードをモジュール分割

    Embedded Frameworkを使って、コードをモジュールとして分割していきます。

    レイヤー化アーキテクチャー等で、各々理想のアーキテクチャーはあると思いますが、ビルド時間の解決に使うことにします。

    • iOS SDKにのみ依存し単体で動作する共通コード
    • アプリで定義したデータモデルやAPIクライアントコード等、複数箇所で利用し変更が頻繁に発生しないコード

    上記両方に共通していえることですが、設定ファイルの読み込みが必要な部分はアプリケーションターゲット側で読み込み、その結果オブジェクトをフレームワーク側で定義したデータモデルに変換して渡すようにした方がテストもしやすいですし、後々依存関係のエラーで悩む必要も無くなります。

    また、再利用可能にすることを考えると、APIのエンドポイントやストレージのパスなども、アプリケーションターゲットから渡すようにした方が良さそうです。

    最終的には下記のようにモジュール分割しました。

    UIExtensionKitとAppKitという名前のフレームワークを作成し、アプリケーションターゲットはそれをインポートして使うようにしました。

    frame "Application Target" {
        [AppContext]
        [App]
    }
    
    package "UIExtensionKit" {
        [Extensions]
    }
    
    package "AppKit" {
        [AppContext Interface]
        [Registory]
        [Data Structures&Logics]
    }
    [App] . [AppContext] : Init
    [App] ... [Extensions] : Use
    [AppContext Interface] <|.. [AppContext] : Implement
    [Registory] *- [AppContext Interface]: DI
    [Data Structures&Logics] . [Registory] : Use
    [App] . [Data Structures&Logics] : Use

    図のApplication Targetというのはアプリケーションのエントリーポイントとなるターゲットです。アプリを起動する場合はこのターゲットを起動します。

    UIExtensionKitは、iOS SDKでのみ実装してアプリの業務ロジックは含まないそれ単体で動作するコードです。カラーパレットの定義やViewコンポーネントが配置されているセルのIndexPathを取得する便利メソッドなどがあります。

    AppKitは、アプリで定義したデータモデルやそれらを操作するビジネスロジックなどを含みます。 APIリクエストのクライアントだったり、APIのレスポンスを加工したりローカルのストレージにシリアライズしたデータを保存したりするコードがあります。

    結果

    改善結果は以下の通りです。差分ビルドの結果は、 UIViewControllerに viewWillAppear(_:) を追加してビルドした結果です。

    コンパイル時間 ビルド種別 内容
    494.4s<約8分> フルビルド 分割していない元のプロジェクト
    247.055s<4分8秒> フルビルド フレームワークを使ってモジュール分割したプロジェクト
    232.8s<3分52秒> 差分ビルド(成功時) 分割していない元のプロジェクト
    86.924s<1分27秒> 差分ビルド フレームワークを使ってモジュール分割したプロジェクト

    まとめ

    差分ビルドが失敗してビルドに時間がかかるという場合は、Frameworkを使ってモジュール分割してみたら改善できたというお話でした。これによって、無駄に残業して仕事をすることもなくなり、効率よく開発できるようになったと思います。

    おまけ(モジュール分割に関して)

    モジュール分割に対応するためには、技術的な課題がいくつかありそれを解決しなければなりません。 ざっと挙げますと

    • アプリケーションターゲットでしか使えないもしくは実体が存在しないオブジェクトやメソッドが存在し、Frameworkやテストターゲットから利用したい場合に簡単にアクセスできそうにない
    • フレームワーク内のコードを動作させるには、アプリケーションターゲットに配置した設定ファイルを読み込む必要がある
    • Objective-Cで書かれた広告や解析SDKを利用する場合、複数のモジュールでインポートすることができない

    今回の場合フレームワーク側は、一方的に利用される側なので、何らかの方法でこういった依存関係を解消する必要があります。

    それには、依存関係逆転の原則(DIP)という方法を使いこの問題を解決しました。フレームワーク側にAppContext Interfaceという抽象インターフェースを定義することにより、アプリケーションの詳細に立ち入らず依存関係のエラーを防ぐことができるようになりました。

    フレームワーク側に用意する抽象インタフェースの例は、下記のようなコードです。

    これを、アプリケーションターゲットやテストターゲットが実装すれば、フレームワーク側は抽象インタフェースを通して処理を行えるようになりました。不必要にSDKをインポートして依存関係のエラーになることもありません。APIのエンドポイントも渡すようにすれば、切り替えのたびにフレームワーク側をビルドし直す必要もありません。

    // AppKit.frameowrk
    public protocol AppContext {
        var apiConfig: APIConfig { get }
        var tracker: Tracker { get }
        var legacyImageSliceFetcher: LegacyObjCImageSliceFetchable { get }
        var bundleResolver: BundleResolvable { get }
        var keychainResolver: KeychainResolvable { get }
        var realmContainer: RealmContainer { get }
        var legacyAPIBridging: LegacyObjCAPIBridging { get }
        var appConfig: AppConfig { get }
        var traits: Traits { get }
        var userDefaultsResolver: UserDefaultsResolvable { get }
        var activity: Activity { get }
    }
    
    public enum TrackerActions {
        case setUser(userId: String?)
        case error(Error)
        case errorWithAdditionalUserInfo(Error, userInfo: [String: Any]?)
    }
    
    // FabricのCrashlyticsはFrameworkの中に重複してリンクできないので、アクションを通じて処理を行わせる(処理の詳細実装自体はこのフレームワークを利用する側にある)
    public protocol Tracker {
        func dispatch(action: TrackerActions)
    }
    

    フレームワークを利用する側(アプリケーションターゲットやテストターゲット)は、下記のようにこのインタフェースを実装すれば良いことになります。

    // アプリケーションターゲット
    import AppKit
    
    final class AppContext: AppKit.AppContext {
        var apiConfig: AppAPIConfig = APIConfig()
    // [...]
    }
    
    // テストターゲット
    import AppKit
    
    final class MockAppContext: AppKit.AppContext {
        var apiConfig: AppAPIConfig = MockAPIConfig()
    // [...]
    }
    

    フレームワーク側で解決できない依存関係は、利用する側で抽象インタフェースを実装することにより解決できるようになりました。

    しかしこの実体をどう渡せば良いのでしょうか?

    下記のようなコンテナをフレームワーク側に用意し、それを利用するようにすればどこからでも呼び出せるようになります。

    public struct Registory {
        private static var context: AppContext!
        public static func setContext(_ context: AppContext) {
            self.context = context
        }
        static func resolve<D>(keyPath: KeyPath<AppContext, D>) -> D {
            return context[keyPath: keyPath]
        }
    }
    

    これでフレームワークの外側から必要な処理を行えるようになりました。

    参考文献

    Redashを0から布教して社員全員に効果検証の文化を根付かせた話

    f:id:A06rp012:20180424185854j:plain (BASEオフィス内の光景)

    初めに

    こんにちは!BASEでBack-end Engineer Groupに所属している菊地陽介です!

    今年度からBASEでは世のエンジニアに興味を持ってもらおうと、積極的に技術ブログを発信していこうという運びとなりました。本記事を読んで少しでも興味を持って頂けましたらぜひ私までご連絡ください!

    さて、私はRedashエヴァンジェリストとしてRedashを社内に普及させましたので、その話について書こうと思います。「Redashって何?」、「Redashサーバってどうやって立てるの?」、「Redashの便利機能は?」などの疑問については先に素晴らしい記事がたくさんありますので、そちらを参考にされてください。

    この記事では、Redashをどのように社内に普及させたのかということについて述べたいと思います。

    背景

    私がBASEに入社したのは、今からちょうど一年前の2017年4月でした。

    当時は何か新機能をリリースしても出しっ放しで、効果があったのかなかったのかの検証が十分になされておらず、次のアクションに活かせていないなと感じていました。日々、「なんだか知らないけど今日は数値が良いような気がする」といった漠然とした感想を持つだけでした。

    ちょうど、前職でRedashを導入したことで社内に良い影響を与えることができたという成功体験があったので、BASEにも導入して普及させようという運びになりました。またRedashを導入することは、数値分析をしやすくなること以外にも様々な効果が期待できるので本記事でご紹介します。

    Redashを導入するメリット

    Redashを導入することで享受できるメリットは多々あるのですが、特に私が一押しのメリットは下記になります。

    非エンジニアがエンジニアに依頼していたデータ抽出が不要になる

    このようなお互い不毛なやりとりけっこう多く行われているのではないでしょうか。

    カスタマーサポートのRさん(以下Rさん): 「ショップAの商品情報一覧が欲しいのですが、データを抽出してもらえないでしょうか?」
    頑固エンジニアのHさん(以下Hさん): 「はい、どうぞ」
    
    〜3時間後〜
    
    Rさん: 「ショップBの商品情報一覧が欲しいのですが、データを抽出してもらえないでしょうか?(さっき頼んだばかりだし頼みづらいな、、)」
    Hさん: 「はい(またかよ)、今忙しいのであとで渡します」
    
    〜3時間後〜
    
    Rさん: (依頼してから3時間経ったけどまだ忙しいのかな、それとも忘れてるのかな、リマインドした方がいいのかな、、、。あー、めんどくせええ。)

    それがRedashが普及するとこんなふうになり、業務効率化がはかれます。

    Rさん: 「ショップAの商品情報一覧が欲しいのですが、データを抽出してもらえないでしょうか?」
    Hさん: 「Redashに任意のショップの商品情報一覧を取得できるクエリを用意したので、これからは自分で抽出できます。」
    
    〜以降〜
    
    Rさん: 「ショップBの商品情報一覧が必要だな。Redashでさくっと抽出しようっと。あー気を遣う必要がなくて楽チン。」
    Hさん: 「前まで頻繁にきてたデータ抽出依頼がこなくなったからコーディングに集中できてハッピー。」
    

    BigQueryの高額請求に怯えなくてよくなる

    BigQueryはクエリが処理したデータ量に応じて課金されるため、うっかり月に150万溶かすという恐れがあります。それ故に気軽にクエリを叩けないという人も多いのではないでしょうか。私もその一人でした。

    しかしRedashでは、強制的にBigQueryのコスト上限設定を適用できるため、不用意にコストのかかるクエリを叩く心配がなくなるのです。

    ちなみにこれが前職でRedashを導入した理由でした。

    Redash普及のためにやったこと

    Redashが導入された当初、BASEのslack上にRedashで作ったグラフをアップしたりしてその素晴らしさを共有しようと努めました。しかしみんなの反応は薄く、兼ねてよりRedashに興味を持っていた一部のメンバーが使うだけでした。

    世の中には便利なツールがたくさんあるにも関わらず、社内全体に普及するようなツールは少ないように感じています。そもそも新しいツールが普及しない理由としては下記のようなものが挙げられると思います。

    1. そもそも存在を知らない
    2. 存在は知っているけどなんだか難しそう
    3. 一部の人しか使ってないから使えなくても大丈夫

    そこで私はマーケティング理論で有名な「キャズム理論」に思いを馳せ、どうやってキャズムを超えるか考えました。

    キャズム理論を応用する

    消費者(社員)は下図の5つのグループに分類でき、アーリーマジョリティー層まで普及させることができれば新商品や新サービスは急激に市場に浸透していくと言われています(これはイノベーター理論というやつ)。 f:id:A06rp012:20180424190041p:plain

    (出典:【キャズム理論】マーケティングの深い溝を乗り越えるには?より引用)

    しかし、アーリーアダプター層とアーリーマジョリティ層の間(エンジニアと非エンジニアの間)には簡単に超えられない深いキャズム(分かり合えない価値観)が存在し、そこをどうやって超えるかというのが新技術(Redash)が普及するかどうかの大きなポイントになります。 f:id:A06rp012:20180424190127p:plain

    (出典:【キャズム理論】マーケティングの深い溝を乗り越えるには?より引用)

    そこで私は、いかにキャズムを超えるかというところに注力しようと心に決め、各部署のコアとなるメンバー(アーリーマジョリティ層)に売り込みにいくことにしました。

    ちなみに私が思い描いたイノベータ理論の層と社内のグループの対応表は下記になります。

    社内のグループ
    イノベーター層 元からRedashに興味を持っていたメンバー
    アーリーアダプター層 エンジニア
    アーリーマジョリティー層 Customer SupportやMarketing、経理・財務のリーダー
    レイトマジョリティ層 Customer SupportやMarketing、経理・財務のメンバー
    ラガード層 Customer Experienceのメンバー

    どのようにRedashを売り込んだか

    私は他部署の人がRedashのメリットを最も享受できるのは先に述べた↓であると考えていました。

    非エンジニアがエンジニアに依頼していたデータ抽出が不要になる

    ですので最もデータ抽出を依頼する頻度が高いCustomer Supportのコアメンバーに、定番のデータを抽出するクエリを全て用意して売り込みに行きました。

    やはり普段データ抽出依頼にストレスを感じていたようで、すぐにRedashを利用することのメリットを理解してもらえました。同様に普段の業務の中でマーケも法務経理もエンジニアへのデータの抽出依頼が度々発生していたので、各部署のコアメンバーに同じ切り口で売り込みにいき、布教活動に勤しみました。

    その後は、私が何も働きかけなくても自動的に社内のあらゆるメンバーに普及していきました。「キャズムを超えた」と感じた瞬間でした。

    まとめ

    Redashが普及した後は「新機能をリリースしたら必ず効果検証しよう」という意識が根付きました。今ではより質の高い効果検証をするためにはどうすれば良いかということをみんな意識するようになり、組織として一段階レベルアップしたなと感じています。他にも「非エンジニアがエンジニアに依頼していたデータ抽出が不要になった」などの業務効率の改善をはかることができ、Redashエヴァンジェリストとしてはやりきったなという思いです。

    今回はキャズム理論を応用させてRedashを社内に普及させたかということについて書きましたが、これは他にも応用が効くと思いますので参考にしていただければと思います。

    おまけ

    社内で新たに一ヶ月で登録されたクエリの数と累積数

    f:id:A06rp012:20180424190217p:plain

    Redash普及率80%

    Redashのアカウント開設数を調べたら80でしたので、BASEチームが約100人であることを考えると普及率は約80%でした。

    EarlGreyを使ってiOSのUIテストを自動で行う

    f:id:tomo358:20180417191124j:plain:w600

    こんにちは。ショッピングアプリ「BASE」のiOSアプリを担当している竜口です。

    背景:あの改修の効果測定用のログ、送られてる?

    ショッピングアプリ「BASE」内で、施策の効果測定やKPIの経過観察で様々なログを使用しているのですが、細かい改修などで特定のログが送られない事象があり、効果測定が出来ずに多部署の作業が止まるということがありました。

    そこでアプリ(今回はiOSのみ)でログが正しく送られていることを保証するために、ログのテストをするようにしました。

    全体の流れ

    次のことをしました。

    1. EarlGreyでUITestを実装
    2. テストの中で送られるログをMockに保持
    3. テスト完了後、Mockにあるログの有無を確認
    4. テストをCircleCIで自動化

    これで改修によるログの影響を保証出来るようになります!!

    1. EarlGreyでUITestを実装

    EarlGrey Referenceは、Googleが作っているiOSのUI automation test フレームワークです。

    書き方は、簡単で以下の例の場合UIButton.accessibilityIdentifierに特定のIDを指定して、テストではselectElementで指定したIDの要素を取得し、要素の有無の確認、要素をタップ等できます。

    class ViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            button.accessibilityIdentifier = "BTN"
        }
    }
    
    class UIAutomationSpec: XCTestCase {
        func testTapButton() {
            EarlGrey
                .selectElement(with: grey_accessibilityID("BTN"))// 要素を指定
                .assert(grey_sufficientlyVisible())// 存在するかを確認
                .perform(grey_tap())// 指定した要素をタップする
        }
    }
    

    2. テストの中で送られるログをMockに保持

    UITest内でログを送る際に、Mockにログを保存する処理を追加します。

    class ViewController: UIViewController {
        @IBAction func didTouchUpButton(_ sender: UIButton) {
            LogManeger.send(log: Log(name: "tap_button"))
        }
    }
    
    struct LogManeger {
        static func send(log: Log) {
            isTest {
                LogMock.store(log)// Mockに保存
            }
            
            // Logを送る
        }
    
        static func isTest(_ doForTest:() -> Void) {
            #if DEBUG
                if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil && enabledUITest {
                    doForTest()
                }
            #endif
        }
    }
    

    3. テスト完了後、Mockにあるログの有無を確認

    テスト開始前に保証するログを指定して、終わった段階でログが揃っているかを確認する。

    class UIAutomationSpec: XCTestCase {
    
        override func tearDown() {
            super.tearDown()
            LogMock.removeAll()
        }
        
        func testTapButton() {
            LogMock.addExpect(Log(name: "tap_button"))// 期待するログを指定
            
            EarlGrey
                .selectElement(with: grey_accessibilityID("BTN"))
                .perform(grey_tap())
            
            XCTAssert(LogMock.verify(), "Logが揃っていません。")// ログが揃っているか確認
        }
    }
    

    4. テストをCircleCIで自動化

    実装したテストをCircleCIで動かすようにして、変更があってもログが送られることを保証できるか確認できるようにしました。 なかでもハマったところがいくつかありましたので紹介します。

    ローカル環境では通るけどCircleCIでは通らなかった

    一番長く戦った凡ミスです。 ローカル環境で実機確認してテスト通るのに、CircleCIで行うと通らないし何故か画面が真っ黒になっていました。

    原因は、ローカル環境とCircleCIで実行している環境が違うので、UITestの結果が環境に依存する場合、結果が変わってしまうことでした。 CircleCIは、どこかの場所でこちらが指定したOS,端末のSimulatorで初回インストールとして起動されテストしています。 なのでローカル環境で確認する時に、CircleCIの環境を揃える必要があり、今回のようにローカル環境で実機確認していると結果が変わる場合があります。

    テスト通らなかったのはわかるがなぜ通らなかったのかわからない

    CircleCIでテスト通らなかった際に、どこの箇所で通らなかったのかはわかりますが、どの画面でなぜ通らなかったかはわかりません。 なので EarlGrey.setFailureHandler(handler: ) で通らなかった時のErrorMessageとその画面のスクショをSlackに送るようにしました。

    f:id:tomo358:20180416192531p:plain

    まとめ

    まだカバーしてる部分は一部重要機能のみですが、テスト導入し今の所ログの不具合も起こらず、何よりUIの保証も最低限できるので安心して開発できるのが良いです!

    BASEを日本一のサービスに成長させる仲間を募集しています!

    BASEでは、Web、アプリ、SRE、データ解析など幅広い職種を募集しています。 ご興味を持った方、カジュアルな面談もできますので、一度オフィスに遊びに来てみませんか?

    base.ac