2022/03/09追記
nextjsのバージョンが12.1.0にアップデートしたため、apiResolverパスが変わりました。
next-test-api-route-handlerはnextjsのリリース翌日にはもう対応版がリリースされていたようなので、しっかりメンテもされているこちらを使うのが良さそうです。
はじめに
Next.jsのAPI Routesを利用する機会があり、公式ドキュメントを見ながらhmhmと実装していました。
なんとかAPIの方は無事に実装でき、さあJestでテストも作成するぞ!となったところで、APIを作成するとき以上に色々とドキュメントを巡る羽目になったので、その記録です。
テスト対象のコード
だいぶテキトーな感じのAPIですが、とりあえずテスト対象を用意したかっただけなので、どうかここは一つ。
今回は用いていませんが、API内で関数を用いている場合、通常通りjest.spyOnやjest.mockで関数をモックすることができます。
通常のGET,POSTリクエスト
とりあえずテストできるコードがあればいいので、適当に足し算するだけのコードです。
import type { NextApiRequest, NextApiResponse } from 'next';
export default (req: NextApiRequest, res: NextApiResponse) => {
const method = req.method;
switch (method) {
case 'GET': {
const { val1, val2 } = req.query;
const result = Number(val1) + Number(val2);
res.status(200).json({ result });
break;
}
case 'POST': {
const { val1, val2 } = req.body;
const result = val1 + val2;
res.status(200).json({ result });
break;
}
default: {
res.status(403).end();
}
}
};
Dynamic API Routesを利用するコード
Next.jsだと、ファイル名を[id].ts
のような形にすることで、動的なpathを設定することができます。これがテストする際にちょっとばかり面倒なので、一応例として用意しておきます。
import type { NextApiRequest, NextApiResponse } from 'next';
import { setCookie } from '../../../modules/cookie';
export default (req: NextApiRequest, res: NextApiResponse) => {
const method = req.method;
switch (method) {
case 'GET': {
const { userId } = req.query;
setCookie(res, 'userId', userId);
res.status(200).json({ header: res.getHeader('Set-Cookie') });
break;
}
default: {
res.status(403).end();
}
}
};
import { serialize, CookieSerializeOptions } from 'cookie';
import { NextApiResponse } from 'next';
export const setCookie = (
res: NextApiResponse,
name: string,
value: unknown,
options: CookieSerializeOptions = {}
) => {
const stringValue =
typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value);
res.setHeader('Set-Cookie', serialize(name, String(stringValue), options));
};
自分で頑張る編
Next.jsのAPI Routesで、作成したAPI用の関数を実行するためのapiResolver
を利用し、nodeのhttpモジュールで簡易的なサーバを作成してリクエストを送ります。
基本系
クエリパラメータをapiResolver
に渡す前にurlから連想配列に変える必要があることがちょっと面倒なくらいでしょうか。
import http, { IncomingMessage, ServerResponse } from 'http';
// nextjsのバージョンが12.1.0より前なら以下のimport
// import { apiResolver } from 'next/dist/next-server/server/api-utils';
import { apiResolver } from "next/dist/server/api-utils/node";
import request from 'supertest';
import handler from '../pages/api/testApi1';
describe('API Test', () => {
let server: http.Server;
const mockedApiPreviewProps = {
previewModeId: '',
previewModeEncryptionKey: '',
previewModeSigningKey: '',
};
// サーバで受け取るリクエストの処理
const requestHandler = (req: IncomingMessage, res: ServerResponse) => {
if (typeof req.url !== 'string') {
throw 'error';
}
const url = new URL(`http://localhost${req.url}`); // URLの形式に合わせる
const query = Object.fromEntries(url.searchParams); // クエリパラメータを連想配列に変換
return apiResolver(req, res, query, handler, mockedApiPreviewProps, false);
};
beforeAll(() => {
// サーバ作成
server = http.createServer(requestHandler);
});
afterAll(() => {
// サーバ停止
server.close();
});
test('API ROUTEのテスト GET', async () => {
const agent = await request.agent(server).get('/testApi1?val1=2&val2=3');
expect(agent.status).toEqual(200);
expect(agent.body.result).toEqual(5);
});
test('API ROUTEのテスト POST', async () => {
const agent = await request
.agent(server)
.post('/testApi1')
.send({ val1: 2, val2: 3 });
expect(agent.status).toEqual(200);
expect(agent.body.result).toEqual(5);
});
});
Dynamic API Routesに対応する
Next.jsのDynamic Routesを使った場合、対応する変数はクエリパラメータと同様のqueryに格納されます。
というわけで、その処理を追加します。
注意点ですが、おそらく該当のAPIによって変数名が異なる(今回はuserId)ので、元のAPIに合わせて変数名を変更する必要があります。
import http, { IncomingMessage, ServerResponse } from 'http';
// nextjsのバージョンが12.1.0より前なら以下のimport
// import { apiResolver } from 'next/dist/next-server/server/api-utils';
import { apiResolver } from "next/dist/server/api-utils/node";
import request from 'supertest';
import handler from '../pages/api/test2/[userId]';
describe('API Test', () => {
let server: http.Server;
const mockedApiPreviewProps = {
previewModeId: '',
previewModeEncryptionKey: '',
previewModeSigningKey: '',
};
const requestHandler = (req: IncomingMessage, res: ServerResponse) => {
if (typeof req.url !== 'string') {
throw 'error';
}
const url = new URL(`http://localhost${req.url}`); // URLの形式に合わせる
const userId = url.pathname.replace('/', '');
const query = Object.assign(Object.fromEntries(url.searchParams), {
userId,
});
return apiResolver(req, res, query, handler, mockedApiPreviewProps, false);
};
beforeAll(() => {
server = http.createServer(requestHandler);
});
afterAll(() => {
server.close();
});
test('API ROUTEのテスト GET', async () => {
const agent = await request.agent(server).get('/1');
expect(agent.status).toEqual(200);
console.log();
expect(agent.headers['set-cookie']).toEqual(
expect.arrayContaining(['userId=1'])
);
});
});
next-test-api-route-handler使う編
自分で一から書くと、上のテストコードを見ればわかる通り、そこそこ面倒です。
なので、それを解決するためにnext-test-api-route-handlerというモジュールがあります。もっとも、モジュールのリポジトリにあるコードをみるとわかりますが、やっていること自体は上の自力実装の場合とあまり変わりません。
ただ、面倒な記述を省くことができるので、こちらを利用した方が楽に済みます。
基本系
わざわざ自分でサーバを作成する必要がなくなるので、かなりコード量が減りシンプルになりました。
import { testApiHandler } from 'next-test-api-route-handler';
import handler from '../pages/api/testApi1';
describe('next-test-api-route-handler test', () => {
test('API ROUTEのテスト GET', async () => {
expect.hasAssertions();
await testApiHandler({
requestPatcher: (req) => (req.url = '/api/testApi1?val1=2&val2=3'),
handler,
test: async ({ fetch }) => {
const res = await fetch({
method: 'GET',
});
expect(await res.json()).toStrictEqual({ result: 5 });
},
});
});
test('API ROUTEのテスト POST', async () => {
expect.hasAssertions();
await testApiHandler({
requestPatcher: (req) => (req.url = '/api/testApi1?val1=2&val2=3'),
handler,
test: async ({ fetch }) => {
const res = await fetch({
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({ val1: 2, val2: 3 }),
});
expect(await res.json()).toStrictEqual({ result: 5 });
},
});
});
});
Dynamic API Routesに対応する
こちらも記述量がだいぶ減り、シンプルになりました。
ただ、こちらもやはり自動でDynamic Routesに対応することができない(そもそも変数名を指定してる部分がファイル名なので……)ため、paramsでurlとは別に値を指定する必要があります。
import { testApiHandler } from 'next-test-api-route-handler';
import handler from '../pages/api/test2/[userId]';
describe('next-test-api-route-handler test', () => {
test('API ROUTEのテスト GET', async () => {
expect.hasAssertions();
await testApiHandler({
params: { userId: 222 },
handler,
test: async ({ fetch }) => {
const res = await fetch({
method: 'GET',
});
const headers = (await res.headers) as Headers;
expect(headers.get('set-cookie')).toEqual('userId=222');
},
});
});
});
おわりに
この記事のテスト方法自体は、主にNext.jsのリポジトリにあったDiscussionをまとめたものです。
1年開かずに似たようなDiscussionが立っているあたり、やはり地味にテスト方法が分かりづらいんだなぁ、と感じました。自分も既存のDiscussionがなければ、まずお手上げだったことでしょう。
その点、next-test-api-route-handlerは非常に手軽にテストすることができます。
惜しむらくは、まだあまり有名どころではなさそう、ということでしょうか。そもそもNext.jsのAPI Routesのテスト専用という時点でだいぶ範囲が限られており、誰もが利用する類のものでもないという理由はありそうですが。
とはいえver1.0.0がリリースされてから一年立っておらず、比較的活発に開発が続いているようなので、今のうちから利用するのも良いかもしれません。
参考
- 基本的な文法
- Next.jsのAPI Routesについて
- API Routesのテストについて