POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

本記事は、原著者の許諾のもとに翻訳・掲載しております。

本稿は、JavaScriptのテストについて最も重要な根拠、用語、ツール、アプローチなどの知識を身に着けることを目的とした簡略版ガイドブックです。本稿で検討する数々の側面に関する最新の秀逸な記事も紹介しつつ、私たちが経験的に得たことも多少付け加えたいと思います。

Facebookによるテスト用フレームワークであるJestのロゴをご覧ください。

見てお分かりのように、このフレームワークは「苦痛のない」JavaScriptのテストをスローガンに掲げています。しかし、 “次のように言う人” もいます。

苦痛のないテストなんてあり得ない。

実際、Facebookはこのスローガンを掲げるだけの素晴らしい理由があります。一般的にJSのデベロッパは Webサイトのテストにあまり満足していません 。JSのテストには制限があり、実装が難しく、低速である傾向があります。

一方、正しい戦略を立てて適切にツールを組み合わせれば、ほぼ全体をカバーすることが可能になり、かなり組織的に、シンプルに、比較的高速にテストを実行できるようになります。

本稿を執筆する際に、メンテナンスされていない優れたライブラリを数多く見つけたということをお伝えしておくのは有意義でしょう。もし会社が、 DalekJS のようにライブイラリを復活させてメンテナンスするように決定すれば、そうしたライブラリには独自の特徴があり、様々な場面で大変有効になる可能性を秘めています。

できれば今後のブログ記事で、こうしたライブラリに関するまとめを執筆できたらと思います。ただ今のところは、現状でメンテナンスされているライブラリに的を絞りましょう。

テストの種類

テストの種類についてより詳細に知りたい方は こちらこちらこちら をご覧ください。
一般的に、最も重要なテストの種類は以下の通りです。

  • 単体テスト – 入力をモック化し、個々の関数やクラスをテストし、出力結果が予想通りであることを確認するテストです。
  • 統合テスト – いくつかのモジュールを組み合わせて予想通りに動作することを保証するテストです。
  • 機能テスト – 製品自体を使って(例えばブラウザを使って)、あるシナリオをテストします。確実に想定した動作をするかといった内部構造は考慮しません。

テストツールの種類

テストツールは次の2つの機能に分けられます。単一の機能を提供するツールもあれば、複数の機能の組み合わせを提供するツールもあります。

理論的には単一のツールで同じ成果を出せるとしても、よりニーズに合った機能を入手するために、複数のツールを組み合わせて使用するのが一般的です。

  1. テストの 環境 を提供する( MochaJasmineJestKarma
  2. テストの構造 を提供する( MochaJasmineJestCucumber
  3. アサーション機能 を提供する( ChaiJasmineJestUnexpected
  4. 生成、 表示 、テスト結果を ウォッチするMochaJasmineJestKarma
  5. 以前の実行時からの変更が意図されたものであることを確認するために、
    コンポーネントやデータ構造を生成し、 スナップショット を比較する( JestAva
  6. モックスパイスタブ を提供する( SinonJasmineenzymeJesttestdouble ]
  7. コードカバレッジ のレポートを生成する( IstanbulJest )
  8. シナリオ実行の管理ができる ブラウザ、または疑似ブラウザの環境 を提供する( ProtractorNightwatchPhantomCasper

上記の用語をいくつか説明しましょう。

テストの構造 とは、テストの構成について言及しているものです。通常テストは、BDD( behavior-driven development(ビヘイビア駆動開発) をサポートするBDD構造で組織され、次のように記述されます。

describe('calculator', function() {
// describes a module with nested "describe" functions
describe('add', function() {
// specify the expected behavior
it('should add 2 numbers', function() {
//Use assertion functions to test the expected behavior
})
})
})

アサーション機能 は、テスト結果が予想通りであることを確認する機能であり、最もよく使われるのは、最初の2つです。

// Chai expect
expect(foo).to.be.a('string')
expect(foo).to.equal('bar')
// Jasmine expect
expect(foo).toBeString()
expect(foo).toEqual('bar')
// Chai assert
assert.typeOf(foo, 'string')
assert.equal(foo, 'bar')
// Unexpected expect
expect(foo, 'to be a', 'string')

ヒント:上級のJasmineのアサーションに関する 素晴らしい記事はこちらから

スパイ は、アプリケーションで使用している関数、もしくは、テスト用に作成した 関数についての情報 を提供してくれます。呼び出しが行われたケースや回数、呼び出し元などの情報です。特に統合テストで内部シナリオを実行する際に、特定の動作を確認するような場合、有効性が発揮されます。例えば、あるプロセスの実行中に計算関数が何回呼び出されたのか、といった場合です。

it('should call method once with the argument 3', () => {
const spy = sinon.spy(object, 'method')
spy.withArgs(3)
object.method(3)
assert(spy.withArgs(3).calledOnce)
})

スタブまたはダビング (映画における代役のようなもの)は、対象モジュールでの動作が確実に予想通りになるよう、 対象の関数を 用意したものに 置き換えます

例えばテストの際に user.isValid() が常にtrueを返すよう、確実を期したい場合は、次のようにします。

sinon.stub(user, 'isValid').returns(true) // Sinon
spyOn(user, 'isValid').andReturns(true) // Jasmine

これはPromiseでも動かせます。

it('resolves with the right name', done => {
const stub = sinon.stub(User.prototype, 'fetch')
.resolves({ name: 'David' })

User.get()
.then(user => {
expect(user.name).toBe('David')
done()
})
})

モックまたはフェイク は、既知の入力値でテストを確実に実行するために、 特定のモジュールや振る舞いを偽装する ことです。例えばSinonでは、予想通りの結果を迅速に得るために、 サーバを偽装 できます。

it('returns an object containing all users', done => {
const server = sinon.fakeServer.create()
server.respondWith('GET', '/users', [
200,
{ 'Content-Type': 'application/json' },
'[{ "id": 1, "name": "Gwen" }, { "id": 2, "name": "John" }]'
])
Users.all()
.done(collection => {
const expectedCollection = [
{ id: 1, name: 'Gwen' },
{ id: 2, name: 'John' }
]
expect(collection.toJSON()).to.eql(expectedCollection)
done()
})

server.respond()
server.restore()
});

スナップショットテスト は、結果のデータ構造を想定したものと比較したい場合に使用します。例えば、以下のJestのテストは”Link”コンポーネントをシミュレーションし、それをJSONとして保存します。

続いて直前の実行結果と比較します。何か変更があれば、デベロッパはその変更が意図したものであるかどうか、同意を求められます。

it('renders correctly', () => {
const linkInstance = (
Facebook )
const tree = renderer.create(linkInstance).toJSON()
expect(tree).toMatchSnapshot()
})

全てを統合する

可能であれば、どの種類のテストにも同じツールを使用することをお勧めします。同じテスト構造とシンタックス(2)、アサーション機能(3)、結果レポートとウオッチ(4)を、全てのテスト環境に対して同じものを使います。一部のテスト、もしくは全てのテストに対して同じテスト環境(1)を使用することさえあります。

必要に応じて、特定の種類のテストだけが実行可能であることに留意してください。

  • 単体テスト では、全て、入力をモック化した(6)ユニットを提供します。そして、アウトプットが予想通りであることを確認し(3)、また、カバレッジレポートツール(7)を必ず使用して、どのユニットがカバーされたのかを確認します。
  • 統合テスト では、重要なモジュール間を横断する内部シナリオを定義します。単体テストと比べると、単にアウトプット(6)をアサーションするのではなく、予想通りの振る舞いであるかをテストするためにスパイやスタブを使用することになるでしょう。また、UI上で、ブラウザや疑似ブラウザ環境により、プロセスと結果が統合されているかをテストできるでしょう。
  • 機能テスト では、ユーザの振る舞いを模したシナリオを実行する際に、プログラム可能なAPIによるブラウザ、または疑似ブラウザ環境(8)を使用します。

概して評判の高いテストツール

JSDom は、JavaScriptにおけるWHATWG DOMや標準HTMLの実装です。言い換えると、JSDomはプレーンJSだけでブラウザ環境をシミュレーションします。

このシミュレーションされたブラウザ環境では、とても高速にテストを実行できます。JSDomの難点は、外部の本物のブラウザ全てをシミュレーションできるわけではないため(例えばスクリーンショットを取得できません)、使用の際にテストに制限がある点です。

JSのコミュニティが迅速に改善を行っていることをお伝えしておきましょう。

Istanbul はどの程度の単体テストをカバーしたのかを教えてくれます。ステートメント、行、関数、ブランチのカバー率をレポートしてくれるので、あとどれくらいカバーするべきなのかが簡単に分かります。

Phantom は、本物のブラウザとJSDom間に、高速で安定的である “ヘッドレス”なWebKitブラウザ を実装します。

本稿を執筆していた頃には非常によく使われていたのですが、GoogleがネイティブなGoogle Chromeブラウザに “ヘッドレス”な実行機能を追加 して以来、主要なクリエイターやメンテナンス担当の Vitaliy Slobodin による メンテナンスは行われていません

Karma によって、本物のブラウザやPhantom、 jsdom 、レガシーブラウザなどの 各種ブラウザでテストを実行 できます。

Karmaは 特殊なWebページを備えたテストサーバ をホストしており、そのページの環境でテストを行えます。このページは数多くのブラウザで実行できます。

BrowserStack のように、リモートでサーバを使ってテストを実行できるようになります。

Chai は最もよく使われているアサーションライブラリです。

Unexpected は、Chaiとは少し異なるシンタックスのアサーションライブラリです。また、拡張性があるので、 unexpected-react などをベースとしたライブラリを備え、より高度なアサーションが実現可能です。unexpected-reactに関する詳細は こちら をご覧ください。

Sion は、JavaScriptの非常に強力なスタンドアローンのテストスパイ、スタブ、モックです。単体テストのあらゆるフレームワークと組み合わせられます。

Testdouble は、新しいライブラリです。Sinonと似ていますが、デザイン、哲学、機能において多少の相違点があり、そのために多くの場面で使い勝手が良くなっています。詳細については こちらこちらこちら をご覧ください。

Wallabyはここでご紹介するに値するツールです。無料ではありませんが、多くのユーザが購入を勧めています。自分のIDE上で実行し(全てのメジャー製品をサポートしています)、コードの変更に関連する部分を実行し、何らかのエラーがあればリアルタイムにコードのそばに表示されます。

フレームワークを選択する

最初に、使用したいフレームワークと、それをサポートするライブラリを選択します。独自のツールを使う必要がなければ、選択したフレームワークが提供するツールを使うことをお勧めします。そうすれば、変更や追加が難しくなることはありません。

– 簡単に言えば、「とにかく始めたい」方や、大きなプロジェクト用に高速なフレームワークを探している方には、Jestをお勧めします。
– 柔軟で拡張性の高い設定を求める方には、Mochaをお勧めします。
シンプルなものを探している方にはAvaをお勧めします。
– かなり低いレベルのものを求める方には、tapeをお勧めします。

以下に、最も重要で評価の高いツールを、長所、短所と共に列挙します。

Jasmine は、テストに必要と思われる全てのものを提供するテストフレームワークです。実行環境、構造、レポート、アサーション、モッキングツールなどです。

  • グローバル – デフォルトでテストのグローバルを作成するので、別途取得する必要はありません。
// "describe" is in the global scope already
// so no these require lines are not required:
//
// const jasmine = require('jasmine')
// const describe = jasmine.describe
describe('calculator', function() {
...
})

これは、Mochaはセットアップが若干難しく、いくつかのライブラリに分割されているということです。しかし、より柔軟性が高く、拡張性もあります。

例えば、 特殊なアサーションロジック が必要な場合、Chaiを選ぶこともできるし、Chaiを自分のアサーションライブラリに置き換えることもできます。Jasmineでも同様のことができますが、Mochaの方がこの意味ではより柔軟性が高いと言えます。

  • コミュニティ – 特有のシナリオをテストできるプラグインや拡張機能が多数あります。
  • 拡張性 – Jasmineにない機能を有するSinonなどのプラグイン、拡張機能、ライブラリ。
  • グローバル – デフォルトでテスト構造のグローバルを作成しますが、ご存じのようにJasmineのようなアサーション、スパイ、モックは作成しません(こうしたグローバルの矛盾に驚く人もいます)。

    Jest はFacebookが推奨する テストフレームワーク です。Jasmineをラップして、それに特徴を加えるので、前述のJasmineに関する内容も全て、適用されます。

>人並外れた量の記事やブログ記事を読んで分かったのは、2016年の終わりには、人々がJestのスピードと利便性に好印象を持っていたことです。

  • パフォーマンス – Jestは、 優れた並列テストの仕組み を実装することで、多くのテストファイルを持つ大規模プロジェクトのテスト時において真価を発揮すると考えられています(私たちの経験や、以下のブログの記事をご覧ください: こちらこちらこちらこちら )。
  • UI – 分かりやすく、便利です。
  • スナップショットテストjest-snapshot は、Facebookによって開発およびメンテナンスされていますが、フレームワークのツール統合の一部として、または適切なプラグインを使用することで、ほとんど全てのフレームワークで使用できます。
  • 改善されたモジュールのモッキング – Jestにより重いライブラリを簡単にモックできるため、テスト速度を改善できます。
  • コードカバレッジIstanbul をベースにした、強力で高速な組み込みのコードカバレッジツールが含まれています。
  • サポート – 2016年末および2017年初頭において、Jestは活発に進展しており、速いペースで改善が続けられています。
  • 開発 – Jestは更新されたファイルのみを更新するので、ウォッチモードでテストが高速に実行されます。

    Ava は、並行にテストを実行するミニマルなテストライブラリです。

  • グローバル – テストグローバルを作成しないため、より詳細にテストを制御できます。
  • シンプルさ – シンプルな構造で、アサーションに複雑なAPIがないながら、多くの先進機能をサポートします。
  • 開発 – Avaは変更が加えられたファイルのみを更新するので、ウォッチモードでテストが高速に実行されます。
  • スナップショットテスト は、 jest-snapshot を使うことで サポート されます。

    Tape は非常にシンプルです。単なるJSファイルで、短く”的確”なAPIを使っており、Node.jsで実行します。

  • シンプルさ – 複雑なAPIを使用しないアサーションとミニマルな構造。Ava以上に簡潔です。
  • グローバル – テストグローバルを作成しないため、より詳細にテストを制御できます。
  • テスト間の 非共有状態 – Tapeでは、テストのモジュール性、およびテストサイクルにおける最大限のユーザ制御を確保するため、”beforeEach”のような関数の使用は推奨されていません。
  • CLIは不要 – TapeはJSを実行できる環境であればどこでも実行できます。

単体テスト

全てをカバーしてください。 Istanbul のようなカバレッジツールを使用して、システム内の全てのモジュールがカバーされていることを確認します。

これらのテストは別々のモジュールをテストするので、(karmaのように)ブラウザを使うのではなく、NodeJSで実行することが推奨されます(JSを実行する場合、ブラウザよりもNodeJSの方が速い)。

結合テスト

統合テスト – 開発中またはそれ以降に、空のテストを含む重要な内部フローのリストをTODOとして作成し、1つずつこれらのテストを実装します。UIモッキングとスナップショットの追加を検討してください。

スナップショットテストは、従来のUI統合テストの有効な代替手段です。特定のプロセス後にUIの各部をテストする代わりに、アプリケーションの各部をスナップショットすることができます。

実際のブラウザでテストを実行する場合、JSDomまたはKarmaの使用を検討してください。

機能テスト

機能テストを目的とした常用ツールは限られており、実装についてもそれぞれ独特なため、判断を下す前に、まずいくつかの実装を試してみることをお勧めします。

– 簡単に言うと、最も簡単な設定で”とにかく始めたい”場合、そして多くの環境を簡単にテストしたい場合は、TestCafeを選びましょう。
– 状況に応じて決めたい場合、コミュニティのサポートを最大限に活用できる場合、そしてJS以外でもテストを書く必要がある場合はSeleniumがお勧めです。
– 例えば、アプリケーションに複雑なユーザインタラクションやグラフィクスがない場合、またはフォームやナビゲーションが多いシステムをテストする場合は、Casperのようなヘッドレスブラウザツールが最速のテストを提供するでしょう。

SeleniumHQ 、通称Seleniumは、 ブラウザを自動化して ユーザの動作をシミュレートします。これはテスト用に特化して書かれたものではなく、APIを使用してブラウザ上でユーザの動作をシミュレートするサーバを公開することで、各種目的に応じてブラウザを制御できます。

Seleniumは、様々な方法で、または様々なプログラミング言語を使用して、あるいは一部のツールを使えば実際のプログラミングなしに制御できます。

しかし、必要に応じて、Seleniumサーバは、Node.JSとブラウザを操作するサーバ間の通信レイヤとして機能する Selenium WebDriver によって制御されます。

Node.js <=> WebDriver <=> Selenium Server <=> FF/Chrome/IE/Safari

テストフレームワークにWebDriverをインポートし、テストをその一部として記述することができます。

describe('login form', () => {
before(() => {
return driver.navigate().to('http://path.to.test.app/')
})
it('autocompletes the name field', () => {
driver.findElement(By.css('.autocomplete'))
.sendKeys('John')
driver.wait(until.elementLocated(By.css('.suggestion')))
driver.findElement(By.css('.suggestion')).click()
return driver.findElement(By.css('.autocomplete'))
.getAttribute('value')
.then(inputValue => {
expect(inputValue).to.equal('John Doe')
})
})

after(() => {
return driver.quit()
})
})

フォークやラップ、修正を通じて拡張できるよう多くのライブラリが作られていますが、WebDriverだけでも十分かもしれませんし、実際に 一部の人たち は変更を加えずにそのまま使用することを提案しています。

確かにWebDriverをラップすると冗長コードが追加され、デバッグが難しくなる可能性がありますし、フォークすることで (2017年の時点で)活発に進行中の開発 の流れから離れてしまう可能性もあります。

それでも、そのまま使用することを好まない人たちもいるのは事実です。以下で、selenium操作のためのライブラリをいくつか見てみましょう。

Protractor は、 Selenium をラップして、Angularの改善されたシンタックスと特殊な組み込みフックを追加するライブラリです。

  • __Angular __ – 特殊なフックがありますが、他のJSフレームワークでも問題なく使用できます。
  • エラー報告 – いい仕組みです。
  • モバイル – モバイルアプリを自動化するサポートはありません。
  • サポート – TypeScriptサポートが利用可能です。ライブラリは巨大なAngularチームが運用、管理しています。

    WebdriverIO には、Selenium WebDriverの独自の実装があります。

  • シンタックス – 非常に簡単で読みやすい。
  • 柔軟性 – テスト用途でも非常に簡潔で、柔軟で拡張可能なライブラリです。
  • コミュニティ – サポートは良好で、プラグインや拡張機能にアクセスできる熱心な開発者コミュニティがあります。

    Nightwatch には、Selenium WebDriverの独自の実装があります。また、テストサーバ、アサーション、およびツールを備えた独自のテストフレームワークを提供します。

  • フレームワーク – 他のフレームワークでも使用できますが、他のフレームワークの一部としてではなく機能テストを実行したい場合に特に有効です。
  • シンタックス – 最も簡単で最も可読性が高いように見えます。
  • サポート – タイプスクリプトのサポートはありません。一般的に、他のライブラリに比べるとサポートは少ないようです。

    Casper は、 PhantomSlimer (Phantomと同じものですが、FireFoxのGecko内にあります)の上に書かれており、ナビゲーション、スクリプティング、テストのユーティリティを提供します。また、PhantomとSlimerスクリプトを作成する際に、複雑で非同期な多数のものを抽象化します。

Casperやその他のヘッドレスブラウザを使うと、UIなしのブラウザで高速に(反面、比較的安定性の低い)機能テストを実行できます。

TestCafe は、Seleniumベースのツールの良好な代替手段です。

>2016年10月、TestCafeのコアライブラリがJavaScriptのオープンソースフレームワークとしてリリースされました。テストレコーダやカスタマーサポートのような非JSツールを提供する 有料版 は依然として利用可能です。

多くの古い記事では、そのコードが非公開でありマイナスだと述べられているため、これは重要です。

これは、Seleniumのようにプラグインを使ってブラウザに接続するのではなく、JSスクリプトとしてブラウザの環境に自身を挿入します。これにより、TestCafeはモバイルデバイスを含むあらゆるブラウザで実行できるようになります。Seleniumでは、どのデバイスやブラウザでも、特殊なプラグインをインストールしなければなりません。

TestCafeはより新しく、よりJSおよびテスト指向です。エラーの行を示すエラー報告システムやセレクタシステムなど、数多くの非常に有用な機能を有しています。

Cucumber も、機能テスト用の便利なフレームワークの1つです。このフレームワークは、わずかに異なる方法で前述の自動化されたテストをアレンジします。

Cucumberは、 Gherkin シンタックスを使って受け入れ基準を記述する側と、それに従ってテストを記述する側にテストを分割することによって、BDDでテストを記述するのをサポートします。

テストは、この記事で取り扱っているJSを含め、フレームワークでサポートされている各種言語で記述可能です。

features/like-article.feature(gherkinシンタックス)

Feature: A reader can share an article to social networks
As a reader
I want to share articles
So that I can notify my friends about an article I liked
Scenario: An article was opened
Given I'm inside and article
When I share the article
Then the article should change to a "shared" state

features/stepdefinitions/like-article.steps.js

module.exports = function() {
this.Given(/^I'm inside and article$/, function(callback) {
// functional testing tool code
})
this.When(/^I share the article$/, function(callback) {
// functional testing tool code
})

this.Then(/^the article should change to a "shared" state$/, function(callback) {
// functional testing tool code
})
}

このような取り決めが、企業内の異なる部署によって行われる共同作業に有効と思われる場合、このツールはきっと役に立つでしょう。

コントリビュート

今回の短いガイドの文章の中で追加または変更した方がいいと思われるものがあれば、ぜひお知らせください。このガイドをできるだけ正確に、完全に、そして有用にしたいと思っているので、喜んで反映させていただきます

まとめ

この短いガイドでは、JSコミュニティで広く使われているテスト戦略とツールを見てきました。この内容を通じて、皆さんがアプリケーションをより簡単にテストできるようになれば幸いです。

なお、今回のガイドではカバーしきれなかったツールや戦略の中にも素晴らしいものはたくさんあります。今回の内容は、ある人にとっては十分かもしれませんが、別の人にとっては不十分な可能性もあります。

>最終的に、昨今のアプリケーションアーキテクチャに関する最良の決定は、活発なコミュニティによって開発された一般的なソリューションパターンを理解し、自分の経験やアプリケーションの特性、そして特別なニーズを理解した上でそれらを組み合わせることでしか成されません。

>ああ、そして一旦書いてみてリライトし、またリライトしてリライトし、そしてダメならまた別のソリューションでテストしてみるしかありません :)


苦痛のないテストなんてあり得ない。

でも、悪くない痛みだ。
技術的負債やスパゲティコード、レガシーコードなどを取り扱う代替案とは対照的にね。

幸せなテストを。

読んでいただき、ありがとうございました。

推奨記事

一般

テストダブル、Sinon

Unexpected.js

テストフレームワークの比較

Jest

Ava

Tape

Selenium

TestCafe