Edited at

JavaScriptのテストフレームワークはAVAが流行り気味らしいので調べてみた

More than 1 year has passed since last update.

(´・ω・`) またテストの話してる

こんにちはみなさん

PHPのテストの話はこれでもかってくらいしてきましたが、ふとPHPを離れると、どれほどテストをしているというのか。

ちょっと前にJavaScriptにオートロードを入れるという俺得な実装をした際にmochaを使ったのですが、本当に基本的な使い方だけで、その深淵が難しそうで手を出せていませんでした。

ところが、@re-fortが言うに「今時のテストはAVAでするんだよ!」ってことらしいので、ちょいと使ってみようと思った次第です


AVA

https://github.com/avajs/ava

ロゴが宇宙っぽくてイカしていますが、mochaみたいなテストフレームワークの一種ですね。

Koajsのissueとかで、mochaからAVAにしようぜって話が出ていました。

starの数も9800あまりと上々の様子です。

売りとしては何よりも「早い!軽い!簡単!」というもののようですが、はたして。


AVAをインストールしてみる

公式にはグローバルインストールしているようですが、私はローカルにインストールする派なので、以下のコマンドでインストールします。

$ npm install --save-dev ava

次にpackage.jsonをいじって、AVAのコマンドを打ちやすくしておきます


package.json

"scripts": {

"test": "ava --verbose"
},


使ってみる

とりあえず適当なテストを作って挙動を確認してみましょう

$ mkdir test

$ vim test/example.js

AVAのテストは、ルート直下のtestディレクトリを再帰的に参照してくれるようなので、そこにテストファイルをおいておきます。

で、テストを書いてみます。


test/example.js

import test from 'ava'

// 基本形
test('ava動作確認', t => {
t.pass()
})

// タイトルはなくてもいい
test(t => {
t.pass()
})


こいつを走らせてみます。

$ npm test

npm info it worked if it ends with ok
npm info using npm@4.2.0
npm info using node@v7.9.0
npm info lifecycle avatest@1.0.0~pretest: avatest@1.0.0
npm info lifecycle avatest@1.0.0~test: avatest@1.0.0

> avatest@1.0.0 test /var/dev
> ava --verbose

✔ example › ava動作確認
✔ example › [anonymous]

2 tests passed

なるほど、テストができたようです


非同期テスト

非同期テストも書けます。イマドキのasync/await使ってみましょう。


syncAsync.js

import test from 'ava'

test('非同期の書き方', async t => {
const bar = Promise.resolve('bar')

t.is(await bar, 'bar')
})

test('同期の書き方', t => {
const bar = 'abcd'

t.is(bar.substr(1), 'bcd')
})


これを走らせてみましょう。

今回は非同期を含むテストのみやりたいので、ファイル指定をしておきます。

# npm test test/syncAsync.js

✔ 同期の書き方
✔ 非同期の書き方

2 tests passed [11:44:59]

ここで注目しておきたいのは、はじめに書いたはずの非同期のテストが結果では後ろに回っているということです。

AVAの大きな特徴として、全てのテストを非同期で並列に実施すると言うものがあります。

おかげで、遅い処理はそのままにして次のテストを始めるという感じになり、効率よくテストが回せるってことみたいです。


AVAの書式


基本的なassert

公式のREADMEから抜き取った感じ、これくらい知っておけばいいと思いました。


variation.js

import test from 'ava'

test('確実に成功 or 失敗', t => {
t.pass()// 絶対成立
t.fail()// 絶対失敗
})

test('値が期待通り', t => {
t.is('abc', 'bcd')// 実際の値 = 期待値
})

test('値が等しくない', t => {
t.not('abc', 'bcd')// 等しくない
})

test('厳密なboolean - true', t => {
t.true(1 < 2)// boolean
t.true(1)
})

test('厳密なboolean - false', t => {
t.false(1 > 2)//boolean
t.false(0)
})

test('曖昧なboolean', t => {
t.truthy(1 < 2)// booleanかそれに相当
t.truthy(1)
t.falsy(1 > 2)
t.falsy(0)
})

test('深さを持ったものが同一', t => {
t.deepEqual([1, 2, 3], [2, 3, 4])// 配列内部の値も含めて等しい
})

test('深さを持ったものが相違', t => {
t.notDeepEqual([1, 2, 3], [2, 4])// 配列内部の値を含めて等しくない
})

test('正規表現', t => {
t.regex('abcdefghijklmn', /fgh/)// 正規表現
t.notRegex('abcdefghijklmn', /fgh/)// 正規表現で調べて含まれていない
})


実際に動かしてみます。

# npm test test/variation.js

✖ 確実に成功 or 失敗 Test failed via `t.fail()`
✖ 値が期待通り
✔ 値が等しくない
✖ 厳密なboolean - true
✖ 厳密なboolean - false
✔ 曖昧なboolean
✖ 深さを持ったものが同一
✔ 深さを持ったものが相違
✖ 正規表現

6 tests failed [11:34:50]

確実に成功 or 失敗
/var/dev/test/variation.js:5

4: t.pass()// 絶対成立
5: t.fail()// 絶対失敗
6: })

Test failed via `t.fail()`

値が期待通り
/var/dev/test/variation.js:9

8: test('値が期待通り', t => {
9: t.is('abc', 'bcd')// 実際の値 = 期待値
10: })

Difference:

"abcd"

厳密なboolean - true
/var/dev/test/variation.js:18

17: t.true(1 < 2)// boolean
18: t.true(1)
19: })

Value is not `true`:

1

厳密なboolean - false
/var/dev/test/variation.js:23

22: t.false(1 > 2)//boolean
23: t.false(0)
24: })

Value is not `false`:

0

深さを持ったものが同一
/var/dev/test/variation.js:34

33: test('深さを持ったものが同一', t => {
34: t.deepEqual([1, 2, 3], [2, 3, 4])// 配列内部の値も含めて等しい
35: })

Difference:

Array [
- 1,
2,
3,
+ 4,
]

正規表現
/var/dev/test/variation.js:43

42: t.regex('abcdefghijklmn', /fgh/)// 正規表現
43: t.notRegex('abcdefghijklmn', /fgh/)// 正規表現で調べて含まれていない
44: })

Value must not match expression:

"abcdefghijklmn"

Regular expression:

/fgh/

実際にはスペースと改行がふんだんに(大げさに)盛り込まれているため、余分な部分は削除してあります。

あと、コンソールコピるとわかりませんが、色付きです。

こんな感じ


API

次に、テストをするに当たり、テストの実施順を決めたり、前後に処理を挟んだりするためのAPIを見てみましょう。


テストの限定/スキップ

任意のテストだけを実施したり、逆にスキップしたりできます。


only.js

import test from 'ava'

test('not run', t => {
t.fail()
})

test.only('will run', t => {
t.pass()
})


実行すると

# npm test test/only.js

✔ will run

1 test passed [12:04:40]

こんな感じです。

ここで注意しておきたいのは、このonlyがある場合、すべての対象ファイルの中で、onlyのついたテストしか実施しません

一方、スキップするときは


skip.js

import test from 'ava'

test.skip('not run', t => {
t.fail()
})

test('will run', t => {
t.pass()
})


こんな感じにします。

onlyの場合は、今このテストだけみたい!っていうときに使って、skipはまだ未実装の部分なんでテストを飛ばす、みたいな使い方になるかもです。


フック

各ファイルで、テストの前と後に処理をねじ込むことができます。

よくある setUp/tearDownですね


hook.js

import test from 'ava'

test.before(t => {
console.log('これはテスト開始前に一回だけ')
})

test.after(t => {
console.log('これは全テスト終了後に一回だけ')
})

test.beforeEach(t => {
console.log('これは各テスト開始前に実施')
})

test.afterEach(t => {
console.log('これは各テスト終了後に実施')
})

test('1', t => t.pass())
test('2', t => t.pass())
test('3', t => t.pass())


挙動はこんな感じです

# npm test test/hook.js

これはテスト開始前に一回だけ
これは各テスト開始前に実施
✔ 1
これは各テスト終了後に実施
これは各テスト開始前に実施
✔ 2
これは各テスト終了後に実施
これは各テスト開始前に実施
✔ 3
これは各テスト終了後に実施
これは全テスト終了後に一回だけ

3 tests passed [12:20:36]

わかりやすくていいですね!

しかし、after系の処理はテストが成功していないと実施されません。

そこで、alwaysというコマンドが用意されていて、これがあるとテストが失敗していても、後処理を実行することができます。


always.js

import test from 'ava'

test.after(t => console.log('一つでも失敗していれば、これは動作しない'))
test.after.always(t => console.log('これはどんなときでも動作する'))
test.afterEach(t => console.log('失敗したテストの直後のみ動作しない'))
test.afterEach.always(t => console.log('どのテストの後でも動作する'))

test(t => t.pass())
test(t => t.fail())


これに対する結果は

  ✔ [anonymous]

失敗したテストの直後のみ動作しない
どのテストの後でも動作する
✖ [anonymous] Test failed via `t.fail()`
どのテストの後でも動作する
これは動作する

1 test failed [01:42:04]

例えば、データベースを含めたテストをしている時、始まりと終わりでトランザクション-ロールバックの仕組みを使って毎回リセットしているときなどは、テストに失敗していてもロールバックを確実にしてほしいので、alwaysを使うことになると思います。


優先・順実行

テストを順番に実行したい場合などはserialを使います。


serial.js

import test from 'ava'

test('1', t => t.pass())
test('2', async t => {
const abc = Promise.resolve('abc')
t.is(await abc, 'abc')
})

// 優先テスト
test.serial('3', async t => {
const efg = Promise.resolve('efg')
t.is(await efg, 'efg')
})
test.serial('4', t => t.pass())


実行結果はこんな感じです

  ✔ 3

✔ 4
✔ 1
✔ 2

serialをつけると、書いた順番ではじめに処理をして、そのあと、serialがついていないテストを実施するようになります。

ここでの注意点は、serialがついている場合は、非同期処理でも順番を崩すことなく実施されるということです。

そのため、テストが並列的に実施できず、実行時間が長引く恐れがあります。

よっぽど順番が大事なテスト以外では使わないほうがいいでしょう。


プランニング

phpunitとかではあまり見ないのですが、perlのテスト見てるとよく出てくるプランニングですね。

テスト内でいくつのアサーションがあるかを予め書いておいて、そこからずれると、何か余分なテストがあったり、テストが足りなくなったりしている、という状況を検知できるみたいです。


plan.js

import test from 'ava'

test('plan設定', t => {
t.plan(1)
t.pass()
})

test('plan設定ミス', t => {
t.plan(1)

t.pass()
t.pass()
})


こんなふうに書くいてテストを実行すると

  ✔ plan設定

✖ plan設定ミス Planned for 1 assertion, but got 2.

/srv/test/example.js:43

42: test('plan設定ミス', t => {
43: t.plan(1)
44:

Planned for 1 assertion, but got 2.

こんな風にアサーションの数が規定と違うと、テスト失敗となります。


まとめ

JavaScriptのテストフレームワークAVAをちょっとさわってみましたが、書き方がわかりやすく、README見るだけでだいたい分かるので、学習コストはなかなか少なくて済みそうです。

また、シンプルな作りで見た目でどこでどういうテストやっているかがわかるので、可読性も良いように思いました。

ただ、test.afterとかはデフォルトで失敗したかどうかにかかわらず実行するようにしてほしいかなって思いました。

今回はこんなところです。