この記事は一休.com Advent Calenrad 2017の2日目です。
宿泊事業本部フロントエンドエンジニアの宇都宮です。
一休.comの宿泊予約サービス(以下、一休)では、以下のようなスタックでWebフロントエンドの開発を行っています。
一休では、主要導線の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-cliでvue 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-webpack
- webpackでのビルド後にmochaを実行する
- アサーション:Node.js組み込みの assert を使用
- vue-test-utils
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という基本的な構造は変わりません。大きな違いは、describe
とit
というグローバル関数が定義されていることが前提になっている点です。この点だけ見るとAVAの方が良いのですが、テストコードの美しさのために開発効率を犠牲にすることはできません…。
今後の展望
ユニットテスト環境の構築については一段落したので、テストケースの増加やカバレッジレポートの定点観測といった、テストの網羅性を高める取り組みを進めていきたいと考えています。
明日はakasakasさんによる「BeautifulSoap4を使ってスクレイピングしつつ、各メソッドを解説してみる」です。