この記事は Next.js Advent Calendar 2019 19日目の記事です。
Next.js + express で実装したカスタムサーバーを supertest を使ってテストする時に supretest に express のインスタンスを渡すことができず困ったので、その時の解決策をまとめました。
発生した問題
次のような Next.js+express によるカスタムサーバーの実装を考えます。
import express from 'express';
export const router = express.Router();
router.get('/banana', (_req, res) => res.json({
banana: [
'美味しいバナナ',
'普通のバナナ',
'腐ったバナナ'
]
}))
import express from 'express';
import next from 'next';
import {router as apiRouter} from '../routes/api';
const port = parseInt(process.env.PORT || '3000', 10);
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
const server = express();
server.use('/api', apiRouter);
server.all('*', (req, res) => handle(req, res));
server.listen(port, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`);
});
});
この時 supertest でテストコードを書く時に、 express のインスタンスが app.prepare()
のコールバック関数の中にラップされてしまい export ができず、テストコードからインスタンスを参照できない問題が発生しました。
import supertest from 'supertest';
// supertest に渡すために express のインスタンスを取得したいけど、ラップされて取得できない
import { server } from './server';
const agent = supertest(server);
test('バナナが取得できること', done => {
agent.get('/api/banana')
.expect(200, {
banana: [
'美味しいバナナ',
'普通のバナナ',
'腐ったバナナ'
]
}, done);
})
解決策
正しくない方法
最初にインスタンスを server/index.ts
から取得する事を諦めて、テストコード内でサーバーをセットアップする方法で対応しました。
import supertest from 'supertest';
import express from 'express';
import { router as apiRouter} from './routes/api';
const server = express();
server.use('/api', apiRouter);
const agent = supertest(server);
test('バナナが取得できること', done => {
agent.get('/api/banana')
.expect(200, {
banana: [
'美味しいバナナ',
'普通のバナナ',
'腐ったバナナ'
]
}, done);
})
ただし、このやり方の場合は実際のコードをテストしていないので非常に不安定なテストになってしまいます。
また、開発が進んでミドルウェアが増えたときに、都度テストコードを修正する必要があり、テストを新しく書く度にセットアップ処理を記述する必要があるので管理がかなり大変になります。
案の定、テストコードの修正が漏れ、テストではミドルウェアを通過しないためパスするが、実際に動かしたらミドルウェアを通過する影響で正しくリクエストが処理がされないバグが発生しました。
しかも、ユニットテストを書いて安心しているので、余計にバグに気付きにくい状態になっていました。
正しい方法
試行錯誤した結果、サーバーのセットアップ処理を別モジュールに切り出すことで、実際のコードを直接テスト出来るようになり安心してテストが出来るようになりました。٩( 'ω' )و
また元のコード自体もサーバーの起動とセットアップで役割を分離できたので、コードの見通しが良くなる意外な恩恵も受けることができました。
同じ問題で困っている人の助けになれば幸いです。
import express from 'express';
import {router as apiRouter} from '../routes/api';
import Server from 'next/dist/next-server/server/next-server';
export const setup = (app: Server, server: express.Express) => {
const handle = app.getRequestHandler();
server.use('/api', apiRouter);
server.all('*', (req, res) => handle(req, res));
return server;
}
import next from 'next'
import express from 'express';
import {setup} from './setup';
const port = parseInt(process.env.PORT || '3000', 10)
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
app.prepare().then(() => {
const server = setup(app, express());
server.listen(port, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
});
})
import next from 'next';
import supertest from 'supertest';
import express from 'express';
import { setup } from './server/setup';
const app = next({ dev: true });
const server = setup(app, express());
const agent = supertest(server);
test('バナナが取得できること', done => {
agent.get('/api/banana')
.expect(200, {
banana: [
'美味しいバナナ',
'普通のバナナ',
'腐ったバナナ'
]
}, done);
});
おまけ
supertest のインスタンス生成もモジュール化しておくと、何かと便利です。
import next from 'next';
import supertest from 'supertest';
import express from 'express';
import { setup } from './server/setup';
const app = next({ dev: true });
const server = setup(app, express());
export const agent = supertest(server);
import { agent } from './libs/agent';
test('バナナが取得できること', done => {
agent.get('/api/banana')
.expect(200, {
banana: [
'美味しいバナナ',
'普通のバナナ',
'腐ったバナナ'
]
}, done);
})