Express アプリのテストの書き方
Node.js のフレームワーク Express を使ったウェブアプリケーション開発における自動テストの書き方をかんたんにまとめました。
利用ツール
今回は React ベースのプロジェクトでよく使われる Jest をテストフレームワークとして使います。
- Node.js 16
- Express 4
- Jest 27
- SuperTest 6
前提
- Node.js 16
手順
プロジェクトを新規に作成するところから始めます。
Express アプリを作成する
プロジェクトディ レクトリを作成し npm init
を実行します。
mkdir express-jest-example-jacd express-jest-example-janpm init
ダイアログがいくつか出てくるので答えます。
完了すれば package.json
が生成されます。
package.json
:
{"name": "express-jest-example-ja","version": "1.0.0","description": "","main": "app.js","scripts": {},"author": "Goto Hayato","license": "ISC"}
ECMAScript modules の設定を行う
ES modules シンタックス( import ~ from 'モジュール'
)を利用するための設定を行います。
なおこの手順はオプショナルで、デフォルトの CommonJS module シンタックス( require()
や module.exports
)を使う場合には不要です。
package.json
に "type": "module"
の設定を追加します。
package.json
:
{"name": "express-jest-example-ja","version": "1.0.0","description": "","main": "app.js","author": "Goto Hayato","license": "ISC","type": "module"}
Express をインストールする
続いて npm
で Express をインストールします。
npm install --save express
メインファイルを作成する
Express ベースのアプリのメインファイルを作成します。
app.js
:
import express from 'express'const app = express()const port = 3000app.get(`/`, (req, res) => {res.send(`Hello World!`)})app.listen(port, () => {console.log(`Example app listening at http://localhost:${port}`)})
これは Express 公式のチュートリアルのサンプルとほぼ同じです。
import
を使っているところだけが公式と異なります。
試しに動かしてみると、ブラウザで「 Hello World! 」と表示されることが確認できるはずです:
node app.jsExample app listening at http://localhost:3000
package.json
にこれを実行するためのスクリプトを追加しておきます。
package.json
:
{// 省略"scripts": {"start": "node app.js"},// 省略}
ここからテストを書いていきます。
自動テストのためのパッケージをインストールする
自動テストを行うためのパッケージをインストールします。
今回は Jest と Jest Puppeteer と SuperTest と start-server-and-test
を使います。
npm install --save-dev jest jest-puppeteer supertest start-server-and-test
jest
: Facebook が開発した人気のテストフレームワークjest-puppeteer
: Jest × Puppeteer のブラウザテストをかんたんに行えるようにするツールsupertest
: HTTP リクエスト用ライブラリsuperagent
ベースのテスト用ツールstart-server-and-test
: ブラウザテストをかんたんに行うためのテスト用ツール
E2E テストを書く
先に Jest Puppeteer を利用して実際のブラウザ( Chrome )を使ったテストを書きます。
__tests__/e2e/server.test.js
:
const port = 3000describe(`Hello World`, () => {beforeAll(async () => {await page.goto(`http://localhost:${port}`);})it(`"Hell World" is there`, async () => {const body = await page.evaluate(() => document.body.textContent)expect(body).toContain('Hello World!')})})
ここで、 describe()
や it()
は Jest が自動で用意してくれるため、どこからかインポー トしてくる必要はありません。
ただし、このままでは変数 page
が未定義でエラーになるので、 jest-puppeteer
を利用する設定を行います:
jest.config.js
:
export default {"preset": "jest-puppeteer"}
上で ECMAScript modules を使う設定をしたのでここでは export default
を使います。
このようにすると、各テストケースの中で page
が利用できるようになります。
このテストは Hello World アプリケーションを起動した状態で実行するものです。
start-server-and-test
を使うと少しその手間が軽減されるので、 start-server-and-test
を使うためのスクリプト設定を package.json
に追加します:
package.json
:
{// 省略"scripts": {"start": "node app.js","jest-puppeteer": "jest __tests__/e2e","test:e2e": "start-server-and-test start http://localhost:3000 jest-puppeteer"},// 省略}
start-server-and-test
の引数の意味は次のとおりです:
start
: サーバーを起動するスクリプトの名前http://localhost:3000
: URLjest-puppeteer
: テストを実行するスクリプトの名前
この設定が追加できたら test:e2e
を実行してテストを実行してみます。
問題なく実行できることが確認できるはずです:
npm run test:e2e> express-jest-example-ja@1.0.0 test:e2e> start-server-and-test start http://localhost:3000 jest-puppeteer1: starting server using command "npm run start"and when url "[ 'http://localhost:3000' ]" is responding with HTTP status code 200running tests using command "npm run jest-puppeteer"> express-jest-example-ja@1.0.0 start> node src/server.jsExample app listening at http://localhost:3000> express-jest-example-ja@1.0.0 jest-puppeteer> jest __tests__/e2ePASS __tests__/e2e/server.test.jsHello World✓ "Hell World" is there (139 ms)Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 0.728 s, estimated 1 sRan all test suites matching /__tests__\/e2e/i.
このままではユニット単位のテストが行いづらいので、 app.js
をパーツに分割していきながら(=リファクタリングしながら)ユニットテストを追加していきます。
app.js
を分割する
app.js
を分割します。
まずは app
の「定義」と「実行」の部分を分離します。
定義の部分はそのまま app.js
に残し、実行の部分を server.js
に移します。
この後分割によりファイルが増えていくのでどちらも src
ディレクトリに移動します。
src/app.js
:
import express from 'express'const app = express()app.get(`/`, (req, res) => {res.send(`Hello World!`)})export default app
src/server.js
:
import express from 'express'import app from './app.js'const port = 3000app.listen(port, () => {console.log(`Example app listening at http://localhost:${port}`)})
package.json
の start
スクリプトもこれにあわせて変更します:
package.json
:
{// 省略"scripts": {"start": "node src/server.js",// 省略},// 省略}
app.js
を app.js
と server.js
に分割した後でも E2E テストがパスすることを ここで確認しておきます:
npm run test:e2e
インテグレーションテストを書く
もともと 1 つだった app.js
を server.js
と app.js
に分けられたので、 app.js
に対するテストを書いていきます。
この粒度のテストを何テストと呼ぶべきかは人によって意見が分かれるところですが、ここでは「インテグレーションテスト」と呼ぶことにします。
ここでは Puppeteer の代わりに SuperTest を使います。
__tests__/integration/app.test.js
:
import request from 'supertest'import app from '../../src/app.js'describe(`Hello world`, () => {test(`GET`, (done) => {request(app).get(`/`).then((res) => {expect(res.statusCode).toBe(200)expect(res.text).toBe(`Hello World!`)done()}).catch((err) => {done(err)})})})
このテストを実行するためのスクリプト設定を package.json
に追加します:
package.json
:
{// 省略"scripts": {// 省略"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest __tests__/integration",// 省略},// 省略}
そのまま Jest を実行す ると import
のところでエラーになってしまうので、環境変数 NODE_OPTIONS
で --experimental-vm-modules
の設定を追加しています。
なお、記事執筆時点で Jest の ECMAScript modules 対応は試験段階にあるので、ここは将来変更される可能性があります。
このテストを実行してみます。 問題なく実行できることが確認できるはずです:
npm run test:integration> express-jest-example-ja@1.0.0 test:integration> NODE_OPTIONS=--experimental-vm-modules jest __tests__/integration(node:1998) ExperimentalWarning: VM Modules is an experimental feature. This feature could change at any time(Use `node --trace-warnings ...` to show where the warning was created)PASS __tests__/integration/app.test.jsHello world✓ GET (13 ms)Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 0.658 sRan all test suites matching /__tests__\/integration/i.
続いて、リ クエストハンドラの部分を別ファイルに分けて、リクエストハンドラを通常の関数とみなしたユニットテストを書いてみます。
リクエストをハンドラを切り出す
app.js
は現在次のような内容になっています。
src/app.js
:
import express from 'express'const app = express()app.get(`/`, (req, res) => {res.send(`Hello World!`)})export default app
このリクエストハンドラの部分を別ファイルに分割します。
src/app.js
:
import express from 'express'import hello from './handlers/hello.js'const app = express()app.get(`/`, hello)export default app
src/handlers/hello.js
:
const hello = (req, res) => {res.send(`Hello World!`)}export default hello
hello
ハンドラを通常の関数として扱いユニットテストを書きます。
__tests__/unit/handlers/hello.test.js
:
// Jest で ECMAScript modules を使用するimport { jest } from '@jest/globals'import hello from '../../../src/handlers/hello.js'test('hello', async () => {const req = {}const res = {send: jest.fn(),}await hello(req, res)expect(res.send.mock.calls.length).toBe(1)expect(res.send.mock.calls[0]).toEqual([`Hello World!`])})
この粒度でテストするときには Jest のモック機能( jest.fun()
)が役に立ちます。
このユニットテストを実行するためのスクリプトを package.json
に追加します:
package.json
:
{// 省略"scripts": {// 省略"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest __tests__/unit",// 省略}// 省略}
テストがパスすることを確認します:
npm run test:unit> express-jest-example-ja@1.0.0 test:unit> NODE_OPTIONS=--experimental-vm-modules jest __tests__/unit(node:7223) ExperimentalWarning: VM Modules is an experimental feature. This feature could change at any time(Use `node --trace-warnings ...` to show where the warning was created)PASS __tests__/unit/handlers/hello.test.js✓ hello (3 ms)Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 0.499 s, estimated 1 sRan all test suites matching /__tests__\/unit/i.
単純にファイルを分けただけなので、インテグレーションテストや E2E テストもパスする状態のままです:
npm run test:integrationnpm run test:e2e
ということで、かんたんではありますが Express ベースのアプリのテストの書き方についてでした。
やったこと
- Hello World だけの小さな Express アプリを作成
- Jest ベースの自動テストを作成
- Puppeteer (Chrome) を使ったテスト
- SuperTest を使ったテスト
- Jest モックを使ったテスト
この記事で使用したコードは GitHub に置いてあるので、興味のある方はそちらもご覧ください。
参考
- Installing Express
- Express プロジェクトのセットアップ
- Using with puppeteer · Jest
- Jest と Puppeteer を使ったブラウザテスト
- How to test Express.js with Jest and Supertest | Through the binary
- Jest と SuperTest で Express のテスト
- Mock Functions · Jest
- Jest でモッキング