Express アプリのテストの書き方

ExpressJavaScript

Node.js のフレームワーク Express を使ったウェブアプリケーション開発における自動テストの書き方をかんたんにまとめました。

利用ツール

今回は React ベースのプロジェクトでよく使われる Jest をテストフレームワークとして使います。

  • Node.js 16
  • Express 4
  • Jest 27
  • SuperTest 6

前提

  • Node.js 16

手順

プロジェクトを新規に作成するところから始めます。

Express アプリを作成する

プロジェクトディレクトリを作成し npm init を実行します。

mkdir express-jest-example-ja
cd express-jest-example-ja
npm 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 = 3000

app.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.js
Example 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 = 3000

describe(`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: URL
  • jest-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-puppeteer

1: starting server using command "npm run start"
and when url "[ 'http://localhost:3000' ]" is responding with HTTP status code 200
running tests using command "npm run jest-puppeteer"


> express-jest-example-ja@1.0.0 start
> node src/server.js

Example app listening at http://localhost:3000

> express-jest-example-ja@1.0.0 jest-puppeteer
> jest __tests__/e2e

 PASS  __tests__/e2e/server.test.js
  Hello World
    ✓ "Hell World" is there (139 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.728 s, estimated 1 s
Ran 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 = 3000

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

package.jsonstart スクリプトもこれにあわせて変更します:

package.json:

{
  // 省略
  "scripts": {
    "start": "node src/server.js",
    // 省略
  },
  // 省略
}

app.jsapp.jsserver.js に分割した後でも E2E テストがパスすることをここで確認しておきます:

npm run test:e2e

インテグレーションテストを書く

もともと 1 つだった app.jsserver.jsapp.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 の設定を追加しています。

参考: ECMAScript Modules · Jest

なお、記事執筆時点で 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.js
  Hello world
    ✓ GET (13 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.658 s
Ran 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 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.499 s, estimated 1 s
Ran all test suites matching /__tests__\/unit/i.

単純にファイルを分けただけなので、インテグレーションテストや E2E テストもパスする状態のままです:

npm run test:integration
npm run test:e2e

ということで、かんたんではありますが Express ベースのアプリのテストの書き方についてでした。

やったこと

  • Hello World だけの小さな Express アプリを作成
  • Jest ベースの自動テストを作成
    • Puppeteer (Chrome) を使ったテスト
    • SuperTest を使ったテスト
    • Jest モックを使ったテスト

この記事で使用したコードは GitHub に置いてあるので、興味のある方はそちらもご覧ください。

参考


アバター
後藤隼人 ( ごとうはやと )

Python や PHP を使ってソフトウェア開発やウェブ制作をしています。詳しくはこちら