CLOVER🍀

That was when it all began.

TypeScript × Vitest × SuperTestでExpressのテストを書いて動かしてみる

これは、なにをしたくて書いたもの?

先日、ECMAScript Modulesを使ったテストを書く時に、VitestをJestの代わりに使ってみるというエントリーを書きました。

VitestでTypeScript × Node.js(ECMAScript Modules)のテストを書く - CLOVER🍀

今回はもう少しNode.jsに踏み込んだ内容として、Expressを使った簡単なアプリケーションを書いて、テストも書いてみましょう。

ちょうど似たようなエントリーを前に書いていたので、こちらをVitestを使うように書き直してみます。

TypeScriptでExpress - CLOVER🍀

Expressを使ったアプリケーションのテストを書く?

前に書いたエントリーは3年前のものなので、今の事情はどうなっているんだろうと調べてみましたが、あまり変わっていなさそうです。

Jestのページでは、やはりSuperTestを勧めています。もっとも、リンク先のページはすでに見れなくなっているようなのでメンテナンスされて
いないとも言えますが…。

Testing Web Frameworks / Express.js

Vitestのドキュメントには、バックエンドフレームワークに関する記述はなさそうです。

Getting Started | Guide | Vitest

設定ファイルでのテスト対象環境がnodeであるかどうか(デフォルトはnode)を気にするくらいでしょうか。

Configuring Vitest / environment

SuperTestはこちら。

GitHub - ladjs/supertest: 🕷 Super-agent driven library for testing node.js HTTP servers using a fluent API. Maintained for @forwardemail, @ladjs, @spamscanner, @breejs, @cabinjs, and @lassjs.

内部的にはsuperagentを利用しています。

GitHub - ladjs/superagent: Ajax for Node.js and browsers (JS HTTP client). Maintained for @forwardemail, @ladjs, @spamscanner, @breejs, @cabinjs, and @lassjs.

3年前に自分が書いた時にはExpressは4.xのままでしたが、SuperTestは4.1.x → 7.0.x、supertestは2.0.x → 10.0.xと大幅にバージョンが
進んでいます…。

ひとまず、進めていきましょう。

環境

今回の環境はこちら。

$ node --version
v20.16.0


$ npm --version
10.8.1

Node.jsプロジェクトを作成する

まずはNode.jsプロジェクトを作成して、必要な依存関係や設定を作成します。

$ npm init -y
$ npm i -D typescript
$ npm i -D @types/node@v20
$ npm i -D prettier
$ npm i -D vitest
$ mkdir src test

ECMAScript Modulesとして、scriptsや依存関係は現時点ではこのように。

  "type": "module",

  ...

  "scripts": {
    "build": "tsc --project .",
    "build:watch": "tsc --project . --watch",
    "typecheck": "tsc --project ./tsconfig.typecheck.json",
    "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch",
    "test": "vitest run",
    "test:watch": "vitest watch",
    "format": "prettier --write src test"
  },

  ...

  "devDependencies": {
    "@types/node": "^20.14.8",
    "prettier": "^3.3.3",
    "typescript": "^5.5.4",
    "vitest": "^2.0.5"
  }

各種設定ファイル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "lib": ["esnext"],
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "skipLibCheck": true,
    "esModuleInterop": true
  },
  "include": [
    "src/**/*"
  ]
}

tsconfig.typecheck.json

{
  "extends": "./tsconfig",
  "compilerOptions": {
    "baseUrl": "./",
    "noEmit": true
  },
  "include": [
    "src/**/*", "test/**/*"
  ]
}

.prettierrc.json

{
  "singleQuote": true,
  "printWidth": 120
}

vite.config.ts

/// <reference types="vitest" />
import { defineConfig } from 'vite';

export default defineConfig({
  test: {
    environment: 'node',
  },
});

簡単なExpressアプリケーションを書く

まずは簡単なExpressアプリケーションを書いてみます。

Expressをインストール。

$ npm i express
$ npm i -D @types/express

依存関係。

  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.14.8",
    "prettier": "^3.3.3",
    "typescript": "^5.5.4",
    "vitest": "^2.0.5"
  },
  "dependencies": {
    "express": "^4.19.2"
  }

簡単なGET、POSTメソッドに対するエンドポイントを作成。いわゆるecho的なものです。

src/app.ts

import express from 'express';
import bodyParser from 'body-parser';

export const app = express();
app.use(bodyParser.json());

app.get('/echo', (req, res) => {
  const message = req.query['message'];

  res.send(`Hello ${message}!!`);
});

app.post('/echo', (req, res) => {
  const message = req.body['message'];

  res.send({ message: `Hello ${message}!!` });
});

サーバーの起動は別ファイルにしておきます。これはテストでの構成を意識しています。

src/server.ts

import { app } from './app.js';

const port = 3000;

app.listen(port, () => {
  console.log(`[${new Date().toISOString()}] server[${port}] startup.`);
});

動作確認します。ビルド。

$ npm run build

起動。

$ node dist/server.js
[2024-08-18T08:24:25.524Z] server[3000] startup.

確認。

## GET

$ curl localhost:3000/echo?message=Express
Hello Express!!


## POST

$ curl -XPOST -H 'Content-Type: application/json' localhost:3000/echo -d '{"message": "Express"}'
{"message":"Hello Express!!"}

OKですね。

テストを書く

次はテストを書きます。

SuperTestをインストール。

$ npm i -D supertest @types/supertest

依存関係はこうなりました。

  "devDependencies": {
    "@types/express": "^4.17.21",
    "@types/node": "^20.14.8",
    "@types/supertest": "^6.0.2",
    "prettier": "^3.3.3",
    "supertest": "^7.0.0",
    "typescript": "^5.5.4",
    "vitest": "^2.0.5"
  },
  "dependencies": {
    "express": "^4.19.2"
  }

では、テストを書いてみます。

test/app.test.ts

import { expect, test } from 'vitest';
import request from 'supertest';
import { app } from '../src/app.js';

test('get /echo', async () => {
  const response = await request(app).get('/echo?message=Express');

  expect(response.status).toStrictEqual(200);
  expect(response.text).toStrictEqual('Hello Express!!');
});

test('post /echo', async () => {
  const response = await request(app)
    .post('/echo')
    .set('Content-Type', 'application/json')
    .send({ message: 'Express' });

  expect(response.status).toStrictEqual(200);
  expect(response.text).toStrictEqual(JSON.stringify({ message: 'Hello Express!!' }));
});

確認。

$ npm test

> express-vitest-getting-started@1.0.0 test
> vitest run


 RUN  v2.0.5 /path/to

 ✓ test/app.test.ts (2)
   ✓ get /echo
   ✓ post /echo

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  17:45:09
   Duration  462ms (transform 40ms, setup 0ms, collect 134ms, tests 34ms, environment 0ms, prepare 79ms)

OKですね。

というわけで、特に問題なくさらっと動かせました。

おわりに

Vitestを使って、ECMAScript ModulesとTypeScriptとExpress、SuperTestを使ったアプリケーションのテストを書いてみました。

たぶんそれほど困らないだろうと思っていたのですが、あっさりと動いたのでやっぱりそうかなと。

どちらかというと、このエントリーを書くにあたってNode.jsのテスト関係のライブラリーを調べ直したりするところが復習になりました。
それほど変わってはいませんでしたけどね。

あと、Expressって4系が長いんですね…。