Help us understand the problem. What is going on with this article?

Next.js + supertest でテストする時のTips

この記事は Next.js Advent Calendar 2019 19日目の記事です。

Next.js + express で実装したカスタムサーバーを supertest を使ってテストする時に supretest に express のインスタンスを渡すことができず困ったので、その時の解決策をまとめました。

t-yng/nextjs-routing-test

発生した問題

次のような Next.js+express によるカスタムサーバーの実装を考えます。

routes/api.ts
import express from 'express';

export const router = express.Router();
router.get('/banana', (_req, res) => res.json({
    banana: [
        '美味しいバナナ',
        '普通のバナナ',
        '腐ったバナナ'
    ]
}))
server/index.ts
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 ができず、テストコードからインスタンスを参照できない問題が発生しました。

server.test.ts
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 から取得する事を諦めて、テストコード内でサーバーをセットアップする方法で対応しました。

server.test.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);
})

ただし、このやり方の場合は実際のコードをテストしていないので非常に不安定なテストになってしまいます。
また、開発が進んでミドルウェアが増えたときに、都度テストコードを修正する必要があり、テストを新しく書く度にセットアップ処理を記述する必要があるので管理がかなり大変になります。

案の定、テストコードの修正が漏れ、テストではミドルウェアを通過しないためパスするが、実際に動かしたらミドルウェアを通過する影響で正しくリクエストが処理がされないバグが発生しました。
しかも、ユニットテストを書いて安心しているので、余計にバグに気付きにくい状態になっていました。

正しい方法

試行錯誤した結果、サーバーのセットアップ処理を別モジュールに切り出すことで、実際のコードを直接テスト出来るようになり安心してテストが出来るようになりました。٩( 'ω' )و

また元のコード自体もサーバーの起動とセットアップで役割を分離できたので、コードの見通しが良くなる意外な恩恵も受けることができました。

同じ問題で困っている人の助けになれば幸いです。

server/setup.ts
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;
}
server/index.ts
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}`)
  });
})
server.test.ts
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 のインスタンス生成もモジュール化しておくと、何かと便利です。

test/libs/agent.ts
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);
test/server.test.ts
import { agent } from './libs/agent';

test('バナナが取得できること', done => {
    agent.get('/api/banana')
    .expect(200, {
        banana: [
            '美味しいバナナ',
            '普通のバナナ',
            '腐ったバナナ'
        ]
    }, done);
})
t-yng
試したことを備忘録として書いています。
https://t-yng.jp/
hamee
happy mobile , easy e-commerce
http://next-engine.net/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした