一休.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を使ってスクレイピングしつつ、各メソッドを解説してみる」です。