如何使用 Jest 和 React-Testing-Library 对 Next.js 13+ App Router API 路由进行单元测试

简介

本文中使用的所有示例都在此存储库中。

将介绍以下内容:

  • 单元测试 Next.js API GET 路由
  • 单元测试 Next.js API POST/PUT/DELETE 路由

单元测试 Next.js GET 路由

📁app/api/items/route.ts:

1
2
3
4
5
6
7
8
9
import { NextResponse } from 'next/server';

export async function GET() {
const items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];
return NextResponse.json(items, { status: 200 });
}

这是一个简单的 GET 端点,它将返回一些项目的 json 对象。在您的情况下,这可能会从数据库或其他一些 API 调用获取数据,然后将其返回给客户端。本文的最后一个示例将介绍这一点。也就是说,让我们看看这个测试会是什么样子:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @jest-environment node
*/
import { GET } from './route';

it('should return data with status 200', async () => {
const response = await GET();
const body = await response.json();

expect(response.status).toBe(200);
expect(body.length).toBe(2);
});

就是这样。我们实际上不需要模拟 GET 函数来测试它。如果需要的话,我们主要需要模拟其中的调用。最后一个例子将涵盖这一点。

一个提示

默认情况下,Jest 使用 node 环境,但是当我们测试 React 组件时,我们需要使用类似浏览器的环境,在这种情况下,我们将使用 jsdom 环境。让我们回顾一下。

我们在 jest.config.ts 文件中指定要用于测试的环境,如下所示:

1
2
3
4
5
const customJestConfig: Config = {
testEnvironment: 'jsdom',

//...other configs
}

问题是我们需要使用 node 环境来测试 API 路由。否则,我们的测试将失败并出现如下所示的错误:

“Request is not defined”。

这主要是因为 Node.js 后续版本中引入了一些新的全局对象,但 jsdom 中尚未包含这些对象,例如 Response 对象。

那么,我们如何在测试React组件时使用 jsdom 环境以及在测试API路由时使用 node 环境呢?

我们所要做的就是告诉 Jest 在路由测试文件中使用 node 环境,方法是在文件的最顶部添加以下注释:

1
2
3
/**
* @jest-environment node
*/

当 jest 看到它将使用节点环境来运行该测试文件中的测试代码。

测试响应主体模式

让我们更进一步地进行前面的测试,并根据模式测试响应主体,以确保我们的 API 将返回我们期望的结果,我们将使用 jest-json-schema 包来测试架构,因此让我们通过运行以下命令来继续安装:

1
npm i jest-json-schema -D

如果您使用 TypeScript,您还需要运行它:

1
npm i @types/jest-json-schema -D

示例 - 测试响应正文模式

📁app/api/items/route.test.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { GET } from './route';
import { matchers } from 'jest-json-schema';
expect.extend(matchers);

it('should return data with status 200', async () => {
const response = await GET();
const body = await response.json();

const schema = {
type: 'object',
properties: {
id: { type: 'number' },
name: { type: 'string' },
},
required: ['id', 'name'],
};

expect(response.status).toBe(200);
expect(body[0]).toMatchSchema(schema);
});

现在我们要确保 API 返回我们期望的属性和数据类型。您可以在此处阅读有关 jest-json-schema 用法的更多信息。

示例 - 使用搜索查询参数测试路线

📁app/api/items/route.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { NextRequest, NextResponse } from 'next/server';

const items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];

export async function GET(req: NextRequest) {
const itemId = req.nextUrl.searchParams.get('Id');

if (!itemId) {
return NextResponse.json({ error: 'Item Id is required' }, { status: 400 });
}

const item = items.find((item) => item.id === parseInt(itemId));

if (!item) {
return NextResponse.json({ error: 'Item not found' }, { status: 404 });
}

return NextResponse.json(item, { status: 200 });
}

📁app/api/items/route.test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { GET } from './route';

it('should return data with status 200', async () => {
const requestObj = {
nextUrl: {
searchParams: new URLSearchParams({ Id: '1' }),
},
} as any;

const response = await GET(requestObj);
const body = await response.json();

expect(response.status).toBe(200);
expect(body.id).toBe(1);
});

it('should return error with status 400 when item not found', async () => {
const requestObj = {
nextUrl: {
searchParams: new URLSearchParams({ Id: '3' }),
},
} as any;

const response = await GET(requestObj);
const body = await response.json();

expect(response.status).toBe(404);
expect(body.error).toEqual(expect.any(String));
});

单元测试 Next.js POST/PUT/DELETE/PATCH API 路由

我们可以应用之前讨论的相同策略来测试这些方法。这里的主要区别是我们将数据传递给路由方法。测试 POST、PUT、DELETE 和 PATCH 方法遵循类似的模式,因为它们本质上归结为底层的 POST 方法。因此,我将仅提供使用 POST 的示例,但您可以以类似的方式测试其他示例。

📁app/api/items/route.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { NextRequest, NextResponse } from 'next/server';

const items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
];

export async function POST(req: NextRequest) {
const requestBody = await req.json();

const item = {
id: items.length + 1,
name: requestBody.name,
};

items.push(item);

return NextResponse.json(item, { status: 201 });
}

📁app/api/items/route.test.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @jest-environment node
*/
import { POST } from './route';
it('should return added data with status 201', async () => {
const requestObj = {
json: async () => ({ name: 'Item 3' }),
} as any;

const response = await POST(requestObj);
const body = await response.json();

expect(response.status).toBe(201);
expect(body.id).toEqual(expect.any(Number));
expect(body.name).toBe('Item 3');
});

原文链接

请我喝杯咖啡吧~

支付宝
微信