本記事の目的
docker-composeを使ってNode.jsアプリのインテグレーションテストを行う #3では、Node.jsで作ったREST APIサーバとPostgreSQLをdocker-compose.ymlで立ち上げ、REST APIを提供できる環境を構築した。
本記事では、テストフレームワークJestを用いて、この環境のインテグレーションテストを行います。
この過程で、インテグレーションテスト特有の事情についても書いていきます。
前回と今回の記事の要点だけつかんでもらえれば、npm scritps 一つで
アプリ起動 -> テスト実行 -> アプリ停止
までをローカル環境でできる内容となっています。
インテグレーションテストの自動化の本質は、
docker-compose と package.json に書かれた内容です。
説明しきれない部分もあると思いますので、気になる方がいれば以下にソースコードを置いておきますので、
e2eフォルダとpackage.jsonに目を通していただければいいと思います。
ソースコード
今回の記事の要点
・Typescriptで実装したテストコードを、Jestフレームワークで実行する
・DBが関係するインテグレーションテストの初期化
・複数のnpm scripts を一つにまとめて実行する
テスト実装環境構築
Typescriptで実装したテストコードをJestで実行するための環境を構築します。
まず、以下のコマンドで必要なモジュールをインストールします。
npm install -D jest ts-jest @types/jest
それぞれの説明を書きます。
- jest・・・jestでテスト行うためのコマンド
- ts-jest・・・Typescriptファイルをjestで実行するためのモジュール
- @types/jest・・・Typescriptファイルがjestの型解決をするためのモジュール
次にテスト環境の設定のために、jest.config.jsを作成します。
以下のコマンドを入力するとjest.config.jsがミニマルで作成されます。
npx ts-jest --init
npx というコマンドは、npm install
でインストールできるモジュールをこのコマンド実行時にだけ利用するというものです。このコマンドのセッションが切れた後では、再度 npx コマンドを実行しなければ使うことができません。
このnpxを用いた理由を2点述べます。
1. npm install -D ts-jest
で ts-jest をインストールしましたが、この時 ts-jest はこのプロジェクト内のnode_modulesのbinの中にインストールされたのであり、グローバルにインストールされたものではありません。つまり、ts-jestはこのプロジェクト内のpackage.jsonのscripts内でだけ使えます。ターミナルで ts-jest を実行してもエラーが返ってきます。
2. それならなぜ、グローバルにインストールしなかったのか?ですが、グローバルにインストールすると当然グローバル環境を汚すことになります。それによって、他のプロジェクト内で使いたいモジュールは今回とは異なるバージョンであってもグローバルにインストールしたものを用いることになってしまい開発に支障をきたしてしまうからです。
よって、npm scripts以外のターミナル等から実行するときには npx コマンドで毎回インストールする方法が良いです。
以上で、必要なモジュールのインストールと設定ファイルの作成ができたので、TypescriptファイルをJestで実行する環境が整いました。
アプリへHTTPリクエストを行う
インテグレーションテストは、HTTPリクエストをアプリに投げて、それがアプリ内で処理された結果が仕様通りのデータとして返ってくるかのテストをおこないます。今回は、このHTTPリクエストをRxJSで扱います。そのためのモジュールである@akanass/rx-http-requestを以下のコマンドでインストールします。
npm i -D @akanass/rx-http-request
このモジュールを用いることでHTTPレスポンスをオブザーバブルとして扱うことでができます。
実装例を以下に示します。
実装内容は、httpのGET を行って、返ってきたレスポンスのステータスコードをコンソールに書き出しています。
import { RxHR } from "@akanass/rx-http-request";
const url = "http://hoge.example.com";
RxHR.get(url).subscribe(
(response) => console.log(response.response.statusCode), // 200
);
インテグレーションテストの際には、自分が立てたREST APIサーバのURLにリクエストして、そのレスポンスが意図したものであるかを確かめます。
インテグレーションテストコード
ユニットテストでは非同期の処理はモックを用いて同期的に値を返しますが、今回は複数のサービスを結合して全体としてうまくいくかをテストするのでモックは用いません。
単純にアプリが提供しているREST APIにリクエストを投げて、仕様通りのレスポンスが返ってくるかのテストを行います。
データベース内の複数のテーブルが関係するようなAPIを大量にテストするとテーブル内にデータが残存して他のテストに影響を与えるような場合がしばしば起こります。そのため、テストファイルの一番初めにすべてのテーブルを空にする処理を行う必要があります。今回は、CARテーブル一つだけ操作をするのでこのような処理は必要ありませんが一応入れておきます。
テーブルを空にするSQLはTRUNCATEコマンドを用いて書けます。
TRUNCATE CAR;
jestを用いた際にはbeforeAllという関数を用いて一度だけ走る処理を書くことができるのでそれを用いて行います。
例)
// doneはdone()が呼ばれるまでこの関数が完了しないために使っています
beforeAll((done) => {
// delete.sqlに上のTRUNCATE CAR;を書いており、それを実行します
const sqlPath = `${path.join(__dirname, "delete.sql")}`;
// ここでSQLを実行して、SQLが完了したイベントを受け取って処理をしています
// addを用いることでsubscribeが完了した後のティアダウンを書くことができます。
// ここでは、ティアダウンの時にdone()を呼び出す処理を書きました。
Database.transaction$(sqlPath).subscribe(
() => { },
() => process.exit(),
).add(done);
});
以上の初期化を終えた後に、POSTとGETのテストを行います。
実装は以下のようになります
内容は複雑に見えますが、
GETでまずデータが無いことを確認 -> POSTでデータを挿入 -> GETでデータが本当に挿入できたかの確認
という内容です。
長いので、実装まで見たくない方は飛ばしていただいて構いません。
describe("POST /cars should insert car data", () => {
const baseUrl = "http://localhost:3000";
test("GET /cars/demio should not get car data", (done) => {
RxHR.get(`${baseUrl}/cars/demio`, { json: true }).subscribe(
(response) => {
expect(response.response.statusCode).toBe(404);
expect(response.body).toBe("Not Found");
},
(e) => fail(e),
).add(done);
});
// POSTでデータを挿入できるかの確認
test("POST /cars should insert car data", (done) => {
const body = {
name: "demio",
maker: "mazda",
};
RxHR.post(`${baseUrl}/cars`, { json: true, body }).subscribe(
(response) => {
expect(response.response.statusCode).toEqual(200);
expect(response.body).toEqual(body);
},
(e) => fail(e),
).add(done);
});
// POSTされたデータをとれるかの確認
test("GET /cars/demio should get car data", (done) => {
const expected = {
name: "demio",
maker: "mazda",
};
RxHR.get(`${baseUrl}/cars/demio`, { json: true }).subscribe(
(response) => {
expect(response.response.statusCode).toBe(200);
expect(response.body).toEqual(expected);
},
(e) => fail(e),
).add(done);
});
});
PUTやDELETEに関しても同様に実装できます。
今回は正常系だけをテストしましたが、リリースされる製品では異常系についても代表的なものは確認しておいたほうがいいと思います。
テストスクリプト作成
前回docker-composeを用いて、アプリを立ち上げるコマンドdocker-compose up --build
を使いました。
そのコマンドをnpm scriptsのstartに登録することで npm run start
と打つことでアプリが立ち上がります。
同様にアプリをティアダウンするコマンドdocker-compose down
を scriptsの end で登録します。
そして、テスト実行コマンドをnpm scripts の test という名前で以下の内容を登録するとnpm run test
でテストを実行することができます
jest ./e2e/test --detectOpenHandles --config=./e2e/jest.config.js
二つのオプションについて説明しておきます
--detectOpenHandles は非同期の処理がテストコード内にあるときに付けるオプションです。
--config は このテストの実行時にどのconfigファイルを見るかの設定です。今回はe2eフォルダ内に入れたものを見ています。
以上の用意の下では、start, test, end の3つのscriptsがそろいました。
テストの度にこの3つを毎回実行するのは面倒なので、一気に実行できるようにします。
そのために、以下のコマンドでnpm-run-allモジュールをインストールします。
npm i -D npm-run-all
このモジュールをインストールすると、npm scripts内で、run-s というコマンドを用いると、他のnpm scriptsを連続で実行するためのnpm scriptsを作ることができます。
今回は、start, test, endを一連で行いたいので以下のnpm scriptsを作成します(下の内容をpackage.jsonのscripts内にか書けば良いだけです)。
"e2e": "run-s -c end start test end"
こうすると、npm run e2e
と打つことで一連の処理が実行されます。
オプションに -c をつけていますが、これはどこかでエラーが起きても次の処理に移るというオプションです。
例えば、test の時にエラーで終わっても -c をつけていることで end が実行されてティアダウンがちゃんと行われます。
まとめ
今回でインテグレーションテスト作成することができました。
一つ目の記事では、Node.jsのインストールを行い、実装環境を構築しました。
二つ目の記事では、REST APIを実装しました(内容はかなり長くなりました)。
三つ目の記事では、docker-composeを用いてアプリとDBを立ち上げました。
最後の今回の記事では、Jestを用いてインテグレーションテストを書きました。
現段階で、npm run e2e
と打つだけで、アプリのインテグレーションテストを行えるようになっています。
このシリーズはここで終わりですが、CI/CDの観点ではこのインテグレーションテストをパイプラインにのせて、デプロイできるかの判断材料にしたりなどに利用できるようにすることもできます。