跳转至内容
There is a version suitable for your browser's language settings. Would you like to go to the english language of the site?
主页文档

API 测试

H1

编写测试代码常常让人感到厌烦,不是吗?我曾经也对它感到头疼,尤其是在开发 Milkio 之前,因为我发现很少有框架在设计时就重视测试。这通常意味着要编写大量的模拟代码和重复的逻辑,或者执行一些毫无意义的工作,仅仅是为了让测试通过。

在设计 Milkio 时,我花费了大量时间让测试变得简单轻松。现在,你可以在开发 API 的同时,使用测试功能来调试你的 API。一旦你调试完成一个功能点,可以其保留,当作测试用例。这样,就不需要再依赖 Postman 或其他类似工具,来调试 API 了!

你可能已经注意到了,在 /src/hello-world/say.ts 文件中,API 下方就附带了一个测试示例。实际上,每一个 API 都可以编写一个测试用例,虽然这并不是强制性的,但却是我们强烈推荐的最佳实践。

export const api = defineApi({
meta: {},
async action(params: { by: string }, context) {
return `Hello ${params.by}!`;
},
});
export const test = defineApiTest(api, [
{
name: "你的测试名称",
handler: async (test) => {
// 你的测试代码..
},
},
]);

每个 API 都可以有多个测试用例,因为 defineApiTest 函数接受一个数组作为第二个参数。测试运行时是顺序执行的,因为 API 测试与单元测试不同,通常依赖于难以完全模拟的资源,如数据库等。因此,我们通过清空数据库等措施来确保测试之间不会相互影响。

测试 API 功能

在测试中,你可以轻松地执行你之前编写的 API,甚至无需输入 API 的路径。你还可以使用 test.reject 来故意让测试失败。

handler: async (test) => {
const result = await test.execute({ params: { by: "Milkio" } });
if (!result.success) return test.reject(`这里是你的 API 测试失败的原因`);
};

使用客户端测试

前文中,我们通过 test.execute 方法来进行测试,通过这种方法本质上是直接执行了你的 API 方法,期间没有网络请求,也不存在 requestresponse 对象。

我们能够依靠于我们的 客户端包 来发送真实的请求进行测试,这种方法可以使得测试过程更贴近实际应用场景。在测试中,我们可以通过 test.client.execute 方法来使用客户端包发送请求:

handler: async (test) => {
const result = await test.client.execute({ params: { by: "Milkio" } });
if (!result.success) return test.reject(`这里是你的 API 测试失败的原因`);
};

默认情况下,我们向 http://localhost:9000/ 发送请求,如果你使用了 Milkio 的 VS Code Extension,它会在你测试运行时自动为你启动你的 HTTP 服务器。如果你修改了端口号,或者想修改客户端包的初始化逻辑,你可以编辑:

/src/api-test.ts
import { createClient } from "client";
export default {
client: () => createClient({ baseUrl: "http://localhost:9000/", memoryStorage: true }),
async onBootstrap() {
// ..
},
async onBefore() {
// ..
}
}

随机生成参数

为测试编写参数总是很繁琐,且很难写得足够全面。你可以使用 test.randParams 方法,来生成随机的参数。

handler: async (test) => {
const params = await test.randParams();
const result = await test.execute({ params });
if (!result.success) return test.reject(`这里是你的 API 测试失败的原因`);
};

随机生成的参数,是依靠你 API 的参数类型推断出来的。如果随机的内容不符合你的预期,你可以通过收窄类型,或者添加更严谨的 Typia 类型标签来使生成的随机参数,更符合你的需要。

测试其他 API

在测试过程中,你可能需要调用其他 API。例如,如果你正在测试一个更新 API,你可能需要先创建数据才能进行更新。此时,你可以通过 executeOther 方法来执行其他 API。如果需要,也可以使用 randOtherParams 来为这些 API 生成随机参数:

handler: async (test) => {
const params = await test.randOtherParams("user/create");
const createResult = test.executeOther("user/create", { params });
if (!createResult.success) return test.reject(`创建过程中失败了`);
params.name = "bar"; // 修改参数
const updateResult = await test.execute({ params });
if (!updateResult.success) return test.reject(`更新过程中失败了`);
};

当然,你同样可以使用 test.client.executeOther(...) 方法,来使用客户端包发送真实的网络请求进行测试。

测试流式 API

你可以通过 test.executeStream 方法来测试流式 API。

handler: async (test) => {
const params = await test.randParams();
const { stream, getResult } = await test.executeStream({ params });
for await (const chunk of stream) {
console.log('chunk:', chunk);
}
const result = getResult(); // getResult 一定要在流读取完成后调用
if (!result.success) return test.reject(`这里是你的 API 测试失败的原因`);
};

如果你想调用其他的流式 API,你可以使用 test.executeStreamOther(...) 方法。

const { stream, getResult } = await test.executeStreamOther("llm/openai", { params });

当然,你也依然可以使用 test.client.executeStream(...)test.client.executeStreamOther(...) 方法,来使用客户端包发送真实的网络请求进行测试。

生命周期挂钩

/src/api-test.ts 文件中,你可以定义一些函数,在测试启动时或每个测试运行之前执行。

例如,我们可能会在这之前清空数据库或 Redis 缓存,以确保每次测试都在干净的环境中进行。

/src/api-test.ts
export default {
async onBootstrap() {
// ..
},
async onBefore() {
// ..
},
};

对于 onBefore,你可以返回一个对象,该对象将被合并到测试对象中。你可以在其中添加一些工具函数,供测试时使用。下面是一个打招呼的示例工具:

/src/api-test.ts
export default {
async onBootstrap() {
// ..
},
async onBefore() {
const sayHello = () => {
console.log("Hello!");
};
return {
sayHello,
};
},
};
/src/apps/hello-world/say.ts
export const test = defineApiTest(api, [
{
name: "你的测试名称",
handler: async (test) => {
test.sayHello(); // 输出: "Hello!"
},
},
]);