Node.jsとテスト

Node.jsとTypeScriptが好きです。また、私が勤めている会社ではTDDでの開発を行っており、ありとあらゆるテストが必須になっています。 私が個人的にNode.jsやTypeScriptで実施しているテストに関して書いてみます。

私たちのTDD

弊社のTDDはフロントエンドのE2Eテスト、API, BFFのE2Eテストに始まり、それぞれのモジュールの単体テストまで一通りのテストを書くようにしています。 特に、E2Eテストで重要なのはアサーションライブラリだと思います。フロントエンドではSeleniumなどを使った画面のテスト、API, BFFはGETなりPOSTなりをしたレスポンスが正しいこと、DBにデータが正しく確認されたことを確認すれば良いだけですが、なかなかそのようなライブラリやフレームワークがないことも多く、Kotlinを使ったE2Eになってしまうことが多いです。単体テストも普通にしています。

Node.jsでTDD

以下では、Node.jsでテストを行う際に試したライブラリやフレームワークについて述べます。

フロントエンドのE2E

Protractor

結構古くからあるフレームワークです。Selenium Webdriverのラッパーと、テストランナーの組み合わせです。 AngularJSの開発チームがAngular用に作ったものらしく、自動でAngularのロードが終わるまで待ってくれたりします。逆にAngularでない画面フレームワークを使っていると、 Angularのロードが終わるまで待ち続けるという罠があります。 ManagedPromiseという黒魔術を持っていて、一見手続き的にテストコードを書くことができ、みやすい、という利点がありました。一方、エラーが起きたときにはどこでエラーになったかを知るのに苦労することが多かったです。 今はasync/awaitを使えばいいので、あまり利用する必要もないかな、と思います。

SSRを行わないウェブアプリが多くなってきた時代ですから、よく引っかかるのが、エレメントが表示されるまで待ってくれなくて、ボタンを押そうとして死ぬ、みたいなことがよくあるので、自分で待つ処理も書く必要があります。

例えば、「ボタンを押したあと、リストにアイテムが3つ追加される」みたいな仕様があったとします。 ボタンを押したあと、リストを取得して、3つあることを確認したいですが、ボタンを押したあとリストが表示されるまでには通信が行われるため、ネットワークの速度が影響してリスト表示までに時間がかかる場合があります。そこで、考えられるのが、「3つ表示されるまで待つ」という方法ですが、アサーションライブラリと別になっているため、3つ表示されるまで待つ処理 と表示されなかったときにエラーにする処理を自力で実装する必要があります。

このへん、JavaのSelenideはアサーションライブラリがSeleniumにくっついているので、エレメントが3つ表示されていること、というアサーションが自然な感じで書けるようになっています。

Puppeteer

Chromeを作った人たちが、Chromeを操作できるようにするためのツールを作った、というライブラリです。 コードをチラッと見てみた限りだと、WebSocketで接続しているようで、Seleniumなどを使っているわけではなく、独自のプロトコルで通信している雰囲気を感じました。 async/awaitで書けるし、わりと書きやすいのですが、Chrome操作ツールなだけあって、E2Eテスト用に便利にしてくれている、みたいなことはありません。これをラップして自分好みのライブラリを作るとよいでしょう。こんな感じで。

なお、当然ですが、Chromeにしか対応していないようですので、マルチブラウザーテストをやりたい人は選ばない方がよいでしょう。

TestCafe

Seleniumを使わないテストフレームワークです。 テストしたいウェブページをTestCafeがプロキシして、中にテストコードを埋め込んでいるようです。 iframeとかで表示しているのかなーと思ったのですが、そんなことはありませんでした。

browser -> testcafe -> テストしたいページ という通信の流れになっていて、TestCafeがテストしたいページにテストに必要な処理やコードを埋め込んでいます。 Selenium RCに似ているのかな。

これの利点としては、WebDriverが不要であり、TestCafeが建てたサーバーにアクセスするだけでテストが実行できてしまうため、スマホやWebDriverに対応していないブラウザーもテストできることが挙げられます。ただし、Android 4.2には対応しておらず(たぶんTestCafeのコード内にブラウザが対応してないメソッドを呼び出したりしている)、大変悲しい思いをしたことを覚えています。

なんとなく、ウェブアプリにJSを差し込んでテストをするというと、そのJSが邪魔をしそうなイメージがあって、ちゅうちょしてしまいます。

Cypress

E2Eテストプラットフォームという感じです。 E2Eテストの操作を記録するツールや実行結果をJenkinsのようにダッシュボードから見ることができます。 動画まで撮ってアップロードしてくれちゃって、、、なんかすごかったです。

ただし、どうやら実行を担ってくれるわけではないようなので、その辺は自力で用意する必要があります。JenkinsやDockerコンテナ 、その他の計算機リソースから実行して、実行結果をダッシュボードにアップロードする、という流れのようです。

なんかの機会に使ってみたいという気持ちになりました。

バックエンドのE2E

バックエンドのE2Eテスト、場所によっていろんな呼び方があると思いますが、要はWebAPIを叩いて、レスポンスやDBを検証するテストのことです。

私は、あんまりこれをいい感じにやってくれるフレームワークを存じ上げないのですが、結局のところWeb APIのエンドポイントをGET/POST/PUTして結果の検証をするだけ、ということで、あんまりテストフレームワークの必要性を感じておりません。

大事なのは以下の3点だと思っています。

  1. リクエストの結果(一般的にはJSON)を検証すること
  2. リクエストの結果、別のAPIにリクエストしたこととリクエストが正しいことを検証できること
  3. リクエストの結果、DBにデータを格納、変更、削除されたことが検証できること

それぞれをみていきましょう。

1. リクエストの結果(一般的にはJSON)を検証すること

Web APIの仕様は破壊的変更がない限り、いつでも好きなように変更して良いと思っています。つまり、あるGETレスポンスにある日突然新たなキーが増えようが問題ないと思っています。 したがって、JSONの検証において、堅牢な(本質的に関係のない変更があってもテストがこけないこと)E2Eテストを行うにはある程度Lazyなアサーションが必要だと考えます。

つまり、以下のような評価でエラーにならないことが重要です。

// APIのレスポンスJSON
const actual = {
  "foo": "bar",
  "hoge": "fuga"
}

const expected = {
  "foo": "bar"
}

assertLazyEqual(expected, actual); // --> OK

これを実現するためのいい感じなライブラリは見つかっておらず、Chaiなどが使えそうなのですが、色々機能があって、調べるのがダルかったので自分で作りました。こちらです。

2. リクエストの結果、別のAPIにリクエストしたこととリクエストが正しいことを検証できること

これは、別のAPIの代わりにモックを立てるかプロキシを立てるくらいしか思いつきませんでした。ちなみに、モックとして超絶に優秀なのがWireMockです。是非使いましょう。 プログラムからモックを用意させることもできるので、動的にレスポンスを変化させることもできるので、超おすすめです。

3. リクエストの結果、DBにデータを格納、変更、削除されたことが検証できること

JavaだとDBUnitみたいな優秀なライブラリがあるようなのですが、JavaScriptでは見つかりませんでした。これも、結局のところDBのデータを期待するデータと比較できればよいので、DBのデータをまるっとJSのオブジェクトに変換して、それを期待するJSONCSVと比較するだけでよいでしょう。

ただし、以下の点に注意して検証できるようにする必要があります。

  1. テーブル内には、テストケースに関係ないゴミデータが入っている可能性があること。
    • 他のテストケースでPOSTしたデータが入っているかもしれませんし、テストを並列実行している場合ならどんなデータがいつ入ってくるかわかったものではありませんね。また、GETのテストをするならば、予めデータを投入しておく必要があります。
    • つまり、結果の検証ではそのような行は無視できる必要があります。
  2. テーブルにはテストでは制御できないカラムがあること。
    • CreatedAtやUpdatedAt、AUTO INCREMENTなidなどが挙げられます。
    • 1.と同様にいらない列は無視できる必要があります。

こういう感じでJSONを比較するライブラリがパッと見つからなかったので、作りました。こちらです。

なお、テストのライブラリの話とは関係ないのですが、テストケースに使うテストデータは他のテストに依存せず独立を保つようにしましょう。並列実行できなくて、いつか泣く日が来ます。私は泣いたことがあります。

フロントエンドの単体テスト

フロントエンドに特化した単体テストというと、ReactやVue.jsのコンポーネントのテストがあげられるでしょう。単なるロジックのテストはフロントもバックも同じですから、バックエンドで書きます。

Reactだと、最近は react-test-renderer を使うようです。Enzymeは死んだそうです。よく知りませんが。

Vue.jsも、組み込みのやつでテストできるみたいですね。これまたよく知りませんが。

コンポーネントのテストは大抵フロントエンドのE2Eで代用してしまうことが多いようです。StoryBookとかを使うときには便利なのでしょうか。

バックエンドの単体テスト

jest を使っておけば、大抵のものが入っていて簡単にテストできます。アサーション、テストランナー、モック機能。これらがあれば、ロジックのテストを書くことには困らないでしょう。

Web APIのE2Eテストのあたりが書きたかったので後半はだいぶ雑になりました。 以上です。よろしくお願いいたします。