はじめに
JavaScriptでテストを書こうとすると、意外と選定コストがかかります。アサーション用に Chai、モック用に Sinon、実行環境に Mocha……と組み合わせを決めるだけで時間が溶けていくことも珍しくありません。「とりあえず動くテスト環境がほしいだけなのに」と感じたことがある方も多いのではないでしょうか。
Jasmineは、その悩みを一気に解消してくれるテスティングフレームワークです。describe・it・expectによるBDD(振る舞い駆動開発)スタイルの記法に加えて、アサーションもスパイ(モック)も最初から標準搭載されています。追加ライブラリを選ぶ必要がなく、npm install一発でテストが書き始められます。
とはいえ、説明より実際に動いているところを見た方が早いと思います。ブラウザ上でJasmineのテストが実行され、成功・失敗がその場で色分け表示される様子を用意しました。先に挙動を確認したい方はこちらからどうぞ。
Jasmineとは
Jasmineは、ブラウザとNode.jsの両方で動作するJavaScript向けのテスティングフレームワークです。Pivotal Labsで開発が始まり、現在はJasmine開発チームによってメンテナンスされているオープンソースプロジェクトで、依存ライブラリなしで単体動作するのが最大の特徴です。
主な特徴
- アサーションもスパイも標準搭載 -
expect().toBe()のようなマッチャーに加えて、spyOn()やjasmine.createSpyObj()によるモック機能が最初から使えます。ChaiやSinonを別途インストールする必要がありません - BDDスタイルの読みやすい記法 -
describeでテスト対象をグループ化し、itで仕様を1文で表現するため、テストコード自体がドキュメントとして機能します - ブラウザ単体でも動く -
jasmine.js・jasmine-html.js・boot.jsを読み込むだけで、Node.jsなしでもブラウザ上でテストとレポート表示が完結します - async/awaitへのネイティブ対応 -
itのコールバックをasync関数として書けるため、Promiseを返す非同期処理もawaitするだけでテストできます
インストール
Node.js環境でCLIとして使う場合は、開発用の依存としてインストールします。
npm install --save-dev jasmine
インストール後、初期化コマンドを実行すると設定ファイルとサンプルの仕様ファイルが生成されます。
npx jasmine init
spec/support/jasmine.jsonが生成され、spec/配下の*.spec.jsファイルが自動的にテスト対象として認識されるようになります。テストの実行は次のコマンドだけです。
npx jasmine
なお、実際にテストを動かしているのはCLIパッケージのjasmineではなく、依存関係として同梱されるjasmine-coreです。ブラウザだけでJasmineを試したい場合は、このjasmine-coreが提供するjasmine.js・jasmine-html.js・boot.jsをそのまま<script>タグで読み込めば、Node.js抜きでも動きます。下のサンプルもこの方法で動いています。
Jasmineのサンプルを動かす
下のサンプルは、Jasmineの標準HTMLレポーターをブラウザ上でそのまま動かしたものです。describeでテスト対象をグループ化し、itの中でexpect().toBe()やexpect().toContain()、expect().toThrow()といったマッチャーを使って検証しています。1件だけisEven(3)の結果に対してわざと誤った期待値を書いており、失敗時の赤い表示も確認できるようにしています。
要点となるテストコードはこれだけです。
describe('sum()', function () {
it('2つの数値を足し算する', function () {
expect(sum(2, 3)).toBe(5)
})
it('負の数にも対応する', function () {
expect(sum(-1, 1)).toBe(0)
})
})
describe('isEven()', function () {
it('配列に含まれる値も検証できる', function () {
expect([1, 2, 3]).toContain(2)
})
it('例外が投げられることも検証できる', function () {
expect(() => throwIfNegative(-1)).toThrow()
})
})
実際に動かせるのが下のサンプルです。成功したテストは緑、失敗したテストは赤で表示され、テスト名や画面上部の「Failures」タブをクリックすると、そのテストだけを絞り込んで再実行した結果を確認できます。
isEven(3)のテストをexpect(isEven(3)).toBe(false)に書き換えて再実行すると、赤かった表示が緑に変わるはずです。逆にsum(2, 3)の期待値を6のようなありえない値に変えると、今度はそちらが失敗に転じます。マッチャーの期待値を変えるだけで結果がその場で変わる感覚を掴んでおくと、以降のセクションのコードも読みやすくなります。
基本的な使い方
Node.js環境でJasmineを使う場合、テストはspec/ディレクトリ配下に*.spec.jsとして置くのが基本です。1つのファイルの中に、テスト対象と検証内容をまとめて書きます。
// spec/math.spec.js
function sum(a, b) {
return a + b
}
describe('sum', () => {
it('2つの数値を足し算する', () => {
expect(sum(2, 3)).toBe(5)
})
it('小数の計算にも対応する', () => {
expect(sum(0.1, 0.2)).toBeCloseTo(0.3)
})
})
これをnpx jasmineで実行すると、コンソールにテスト結果が表示されます。beforeEach・afterEachを使えば、各テストの前後に共通のセットアップ・後片付けを挟むこともできます。
describe('カウンター', () => {
let count
beforeEach(() => {
count = 0
})
it('初期値は0', () => {
expect(count).toBe(0)
})
it('インクリメントできる', () => {
count++
expect(count).toBe(1)
})
})
toBe(厳密等価)、toEqual(構造の一致)、toContain(配列・文字列への包含)、toThrow(例外送出)、toBeNull・toBeDefined(値の有無)など、Jasmineには一通りのマッチャーが揃っており、.notを前置すれば否定条件も書けます。
実践的なユースケース
Jasmineで非同期処理をテストする
APIレスポンスやタイマー処理など、Promiseを返す非同期関数はプロジェクトのどこにでも登場します。Jasmineのitはコールバックをasync関数として書けるため、Promiseを返す処理はawaitするだけでテストでき、従来のdoneコールバックによる書き方も引き続きサポートされています。
it('IDに対応する名前を非同期で返す', async function () {
const name = await fetchUserName(1)
expect(name).toBe('Alice')
})
fetchUserNameの中のsetTimeoutの待ち時間を伸ばしても、awaitしている限りテストは正しく待ってから判定してくれます。逆にdone()を呼び忘れると、そのテストはタイムアウトして失敗になる点も試してみると挙動がつかめるはずです。
スパイでモックを使ったテストを書く
「特定の関数が呼ばれたかどうか」を検証したい場面は多くあります。Jasmineではjasmine.createSpyObj()でスパイオブジェクトを作るだけでよく、Sinonのような別のモックライブラリを追加する必要がありません。toHaveBeenCalledWithやtoHaveBeenCalledTimesで呼び出し内容まで細かく検証できます。
it('メッセージがあればsendを呼び出す', function () {
const service = jasmine.createSpyObj('service', ['send'])
notifyUser(service, 'こんにちは')
expect(service.send).toHaveBeenCalledWith('こんにちは')
})
notifyUserの条件をmessage.length > 0からmessage.length > 3のように変えると、「こんにちは」を渡したテストの結果がどう変わるか、あわせて試してみてください。スパイは実際のオブジェクトを一切用意せずに済むため、外部サービスに依存する処理でも安心してテストを書けます。
まとめ
Jasmineは、アサーションとスパイ(モック)を標準搭載したテスティングフレームワークで、ライブラリの組み合わせを悩む必要がありません。describe・it・expectによるシンプルな記法でBDDスタイルのテストを書け、Node.jsのCLIとしても、ブラウザ単体でも動作します。async/awaitへのネイティブ対応やspyOnによるモックも標準機能の範囲で完結するため、「とりあえずテストを書き始めたい」というときの選択肢として十分な実力を持っています。
まずは今回のサンプルを手元で書き換えながら、npx jasmine initで実際のプロジェクトにも導入してみてください。