Skip to content
当前有符合你浏览器所设置语言的版本,是否前往zh-CN版本的网站?
There is a version suitable for your browser's language settings. Would you like to go to the zh-CN language of the site?
HomeDocument

API Testing

H1

Writing test code can often be tedious, isn’t it? I used to find it a headache too, especially before developing Milkio, because I found that few frameworks emphasized testing during design. This usually means writing a lot of mock code and repetitive logic, or doing some meaningless work just to pass the tests.

When designing Milkio, I spent a lot of time making testing simple and easy. Now, you can debug your API while developing it. Once you debug a feature, you can keep it as a test case. This way, you don’t need to rely on tools like Postman to debug the API anymore!

You may have noticed that there is a test example below the API in the /src/hello-world/say.ts file. In fact, you can write a test case for each API, although it is not mandatory, it is strongly recommended as a best practice.

export const api = defineApi({
meta: {},
async action(params: { by: string }, context) {
return `Hello ${params.by}!`;
},
});
export const test = defineApiTest(api, [
{
name: "Your Test Name",
handler: async (test) => {
// Your test code..
},
},
]);

Each API can have multiple test cases because the defineApiTest function accepts an array as the second parameter. Tests are executed sequentially because API testing is different from unit testing and usually relies on resources that are difficult to fully simulate, such as databases. Therefore, we ensure that tests do not affect each other by clearing the database between tests.

Testing API Features

In testing, you can easily execute the API you have written before, even without entering the API path. You can also use test.reject to intentionally fail the test.

handler: async (test) => {
const result = await test.execute({ params: { by: "Milkio" } });
if (!result.success) return test.reject(`Here is the reason why your API test failed`);
};

Using Client Testing

Previously, we used the test.execute method for testing, which essentially directly executes your API method without network requests, request, or response objects.

We can rely on our client package to send real requests for testing, making your tests closer to real-world scenarios.

In testing, you can use the test.client.execute method to send requests using the client package:

handler: async (test) => {
const result = await test.client.execute({ params: { by: "Milkio" } });
if (!result.success) return test.reject(`Here is the reason why your API test failed`);
};

By default, requests are sent to http://localhost:9000/. If you use the Milkio VS Code Extension, it will automatically start your HTTP server for testing. If you change the port number or want to modify the client package’s initialization logic, you can edit:

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

Generating Random Parameters

Writing parameters for tests is always tedious and difficult to be comprehensive. You can use the test.randParams method to generate random parameters.

handler: async (test) => {
const params = await test.randParams();
const result = await test.execute({ params });
if (!result.success) return test.reject(`Here is the reason why your API test failed`);
};

The randomly generated parameters are inferred based on the types of your API parameters. If the random content does not meet your expectations, you can narrow down the types or add more rigorous Typia type tags to generate more suitable random parameters.

Testing Other APIs

During testing, you may need to call other APIs. For example, if you are testing an update API, you may need to create data before updating. In this case, you can use the executeOther method to execute other APIs. If needed, you can also use randOtherParams to generate random parameters for these APIs:

handler: async (test) => {
const params = await test.randOtherParams("user/create");
const createResult = test.executeOther("user/create", { params });
if (!createResult.success) return test.reject(`Failed during creation`);
params.name = "bar"; // Modify parameters
const updateResult = await test.execute({ params });
if (!updateResult.success) return test.reject(`Failed during update`);
};

Of course, you can also use the test.client.executeOther method to send real network requests for testing.

Testing Streaming APIs

You can test streaming APIs using the test.executeStream method.

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 must be called after the stream has been fully read
if (!result.success) return test.reject(`This is the reason for your API test failure`);
};

If you want to call other streaming APIs, you can use the test.executeStreamOther method.

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

Of course, you can still use the test.client.executeStream and test.client.executeStreamOther methods to send real network requests for testing using the client package.

Lifecycle Hooks

In the /src/api-test.ts file, you can define some functions to run before testing starts or before each test run.

For example, you may want to clear the database or Redis cache before each test to ensure that each test runs in a clean environment.

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

For onBefore, you can return an object that will be merged into the test object. You can add some utility functions for testing. Here is an example of a greeting tool:

/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: "Your Test Name",
handler: async (test) => {
test.sayHello(); // Output: "Hello!"
},
},
]);