5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Next.jsAdvent Calendar 2019

Day 19

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

Last updated at Posted at 2019-12-18

この記事は 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);
})
5
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?