一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

一休.comのJavaScriptユニットテスト環境

この記事は一休.com Advent Calenrad 2017の2日目です。

宿泊事業本部フロントエンドエンジニアの宇都宮です。

一休.comの宿泊予約サービス(以下、一休)では、以下のようなスタックでWebフロントエンドの開発を行っています。

  • 言語:ES 2017
  • ライブラリ・フレームワーク:古いところはjQuery、新しいところはVue.js
  • ビルドパイプライン:Webpack + Babel

一休では、主要導線のE2Eテストは整備されています*1。一方、フロントエンド(JavaScript)のユニットテストは発展途上といったところです。

本記事では、一休のJSユニットテスト環境の変遷と現状について紹介します。

AVA

2017年4月の時点で、一休のJSユニットテスト環境は以下のような状況でした。

  • テスティングフレームワーク:AVA
  • Babelでビルドされているコード:1,000行程度?
  • テストの数:ほとんどない

AVA は、Babelによるビルド機能を内蔵したテスティングフレームワークです。定番フレームワークMochaに比べて、以下のような特長があります。

  • 暗黙的なグローバルへの依存がない
  • テストが並列に実行される
  • テスト結果の出力がわかりやすい(power-assert)

はじめのうちは、AVAで快適にユニットテストを書いていました。しかし、Babelでビルドされるコードが増えるにつれ、コンパイル時間が問題になり始めました。

一例として、以下のような簡単なJavaScriptコードをテストしてみます。

/**
 * 金額のフォーマットを行う
 * ※Number.prototype.toLocaleString() は古いブラウザでは動かないので正規表現を使っている
 *
 * Usage: money(10000) => '10,000'
 *
 * @param value
 * @returns {string}
 */
export default value => String(value).replace(/([0-9])(?=([0-9]{3})+(?![0-9]))/g, '$1,');

AVAを使うと、テストは以下のように書けます(describeはAVA組み込みの関数ではなく、ava-specライブラリの提供する関数です)。

import { describe } from 'ava-spec';
import money from 'vue/Filters/money';

describe('Money Filter', async (it) => {
  it('金額をフォーマットして返す', async (t) => {
    t.is(money(100), '100');
    t.is(money(1000), '1,000');
    t.is(money(10000), '10,000');
    t.is(money(100000), '100,000');
    t.is(money(1000000), '1,000,000');
  });
});

このテストの実行時間を計測すると、以下のような結果になりました。

$ time npm run ava money.test.js

  √ Money Filter 金額をフォーマットして返す

  1 test passed [12:36:03]


real    0m35.385s
user    0m0.061s
sys     0m0.090s

単純なテストなのに 35秒 もかかっています。実行時間のうち、9割以上を占めるのはコードのコンパイル時間です。AVAは全てのソースコードをビルドしてからテストを実行開始するため、起動が遅くなっています。

AVAのwatchモードを使えばコンパイル時間を減らすことはできますが、起動が遅いという根本的な原因は解決されません。

プリコンパイルによる高速化も検討しましたが、テストの実行手順が複雑化してしまいます。

そもそも、我々がやりたいのはテストを書くことであって、テストの実行環境を最適化することではありません。ということで、AVAからの移行を検討しました。

移行対象の検討

今後Vue.jsを使ったコードが増えることが予想されるため、移行対象の検討に当たっては、Vue.jsとの相性が良いことを念頭に置きました。

参考(1) vue-cli

vue-clivue init webpackすると、以下のスタックでユニットテスト環境が構築されます。

Karmaはブラウザを使ったテストランナーですが、ヘッドレスブラウザのPhantomJSを使うことでCI環境でも実行できるようにしています。

※以前検討を行った時点では、Karma + Mochaのみでしたが、最新のvue-cliでは「Jest」「Karma and Mocha」から選べるようになっています。

参考(2) vue-test-utilsのexample

公式のテストユーティリティvue-test-utilsでは、以下のテスティングフレームワークを使用したexampleを提供しています。

それぞれの実行時間を計測してみました。各exampleで全く同じテストを実行しているわけではないですが、参考にはなると思います。

計測結果は下記で、tapeが最速、AVAは他より倍くらい遅い、ということがわかります。

Runner Time
Jest 10.584s
Mocha 8.008s
tape 6.311s
AVA 21.106s

tapeが最速で、AVAが群を抜いて遅い、という傾向は、vue-unit-test-perf-comparisonとも一致します。

Runner 10 tests 100 tests 1000 tests 5000 tests
tape 2.32s 3.49s 9.28s 38.31s
jest 2.44s 4.50s 21.84s 91.91s
mocha-webpack 2.32s 3.07s 10.79s 38.97s
karma-mocha 7.93s 11.01s 33.30s 119.34s
ava 19.05s 73.44s 625.15s 7161.49s

移行対象の決定

移行対象は、下記のスタックにしました。

Mochaにしたのは、vueコミュニティでは最も広く使われている(vue-cliが使っている)ためです。

Karmaは一応導入しましたが、あまり使っていません。browser-envを使えば、DOMを使っているコードのテストをNode.js上でも実行できるからです。また、Karmaはブラウザの起動コストの分、実行が遅くなるのも懸念点です。

どのくらい改善するか計測する

AVAで35秒かかっていたテストが、どれくらいの実行時間になるか計測してみます。

$ time npm run mocha money.spec.js

 WEBPACK  Compiling...

 WEBPACK  Compiled successfully in 963ms

 MOCHA  Testing...


  Money Filter
    √ 金額をフォーマットして返す


  1 passing (3ms)

 MOCHA  Tests completed successfully


real    0m5.217s
user    0m0.000s
sys     0m0.105s

35秒 => 5秒と、大幅に改善していることがわかります! mocha-webpackを使うことで、テストの実行に必要なファイルだけをビルドしているのが速度の改善に寄与しています。

Mocha移行後のテストコード

さきほどのmoney関数のテストを、Mocha + assertで書くと以下のようになります。

/* global describe, it */
import assert from 'assert';
import money from '@js/vue/Filters/money';

describe('Money Filter', () => {
  it('金額をフォーマットして返す', () => {
    assert.equal(money(100), '100');
    assert.equal(money(1000), '1,000');
    assert.equal(money(10000), '10,000');
    assert.equal(money(100000), '100,000');
    assert.equal(money(1000000), '1,000,000');
  });
});

見ての通り、describe~itという基本的な構造は変わりません。大きな違いは、describeit というグローバル関数が定義されていることが前提になっている点です。この点だけ見るとAVAの方が良いのですが、テストコードの美しさのために開発効率を犠牲にすることはできません…。

今後の展望

ユニットテスト環境の構築については一段落したので、テストケースの増加やカバレッジレポートの定点観測といった、テストの網羅性を高める取り組みを進めていきたいと考えています。

明日はakasakasさんによる「BeautifulSoap4を使ってスクレイピングしつつ、各メソッドを解説してみる」です。

単純なコードでアプリ内のコンバージョン経路を計測する

この記事は一休.comアドベントカレンダー2017の1日目です。

一休レストランiOSアプリを開発しているid:ninjinkunです。

アプリを改善する計画を立てる際に、どこから手を着けたら良いか優先順位を決める必要があります。Eコマースなどコンバージョン(以下CV)がはっきり定義できるアプリの場合には、実際のCVに繋がっている画面とフローを改善するのが近道でしょう。

しかしCVに繋がる経路がどこなのか調べるためには、依存関係があるイベントを計測する必要があり、これは意外と面倒です。導入しているGoogle Analyticsの目標到達プロセスやゴールフロー機能を使えばできるような気がするのですが、設定が複雑かつ結果が出るのに時間がかかるので、残念ながら自分は使いこなせたことがありません*1

f:id:ninjinkun:20171130201906p:plain

計測

しかしプログラマの目で見ると、予約前に表示された画面と予約完了画面を結びつけるのはそんなに難しくなさそうに見えます。そこでとても単純な方法ではありますが、直接的にアプリ内のCV経路を計測するために、アプリに直接トラッキングのためのコードを埋め込みました。

enum CVRoute {
    case search, favorite, reserveHistory, message
}

// 単純なコードというのはそう、シングルトンです
struct CVRouteManager {
      static let sharedManager = CVRouteManager()
      var lastRoute: CVRoute?
}

このシングルトンにそれぞれの画面を表示した際に経路を記録し

class ReserveHistoryViewController: UIViewController {
    func viewDidAppear(animated: Bool) {
        ...
        CVRouteManager.sharedManager.lastRoute = . reserveHistory
        ...
    }
}

予約が行われた際にGoogle Analyticsのカスタムディメンジョンに載せて送信します。

class ReserveCompleteViewController: UIViewController {
    static let LastRouteDimension = 20

    func viewDidAppear(animated: Bool) {
        ...
        // 実際はトラッキング用のクラスに分離しているが、イメージはこんな感じ
        let tracker = GAI.sharedInstance().defaultTracker
       ...
        if let lastRoute = CVRouteManager.sharedManager.lastRoute {
            tracker.set(GAIFields.customDimension(for: LastRouteDimension), value: lastRoute)
        }
        ...
        traker.send(...)
    }
}

結果

製品にこのコードを埋め込んで一週間ほど動かしてみたところ、一休レストランのiOSアプリではお気に入りからのCVが検索に次いで多いという結果が得られました。

このデータとユーザーインタビューを元に、アプリを使ってお店を探すユーザーの行動をイメージしてみると、

  1. お店を決めるためにまず検索
  2. 気になったお店をお気に入りに登録
  3. 1~2を繰り返す
  4. お気に入りに登録した中から、一緒に行く友だちにリンクを共有して相談
  5. お店が決まったら、お気に入りから予約←これが計測したCV前画面

というように、お気に入りを使いながらお店を決めるストーリーを想像することができます。そして最終的にはCV数の多さが決め手になり、優先度を上げてこのストーリーを検証、強化するという意志決定を行うことができました。

おわりに

以上、単純なコードで手軽に経路計測を行う方法でした。Google AnalyticsやFirebase等の計測サービスでもっと簡単にできる方法をご存知でしたら、ぜひ教えてください。

最後に唐突に宣伝ですが、クリスマスや忘年会のご予約に、一休レストランアプリをぜひご利用ください。

明日は @ryo511 さんによる「Vue.jsのユニットテストについて」です。

*1:アプリの場合はWebと違い、タブやモーダルビューの存在により、複数の画面を並行に操作して、最後は購入などのCVに到達するということが起こります。このため、トラッキングサービスが出してくるイベントの前後関係を素直に信じられないという問題もあります