Introduction
Recently, I took time to write unit tests to ensure if my Slack Web API client library works as expected.
As someone who has developed web services for a long time, I've often found mocking HTTP requests in test scenarios to be bothersome and less flexible than I would prefer.
The Game Changer
However, I discovered a great combination that transformed my API call testing in TypeScript: Vitest and Mock Service Worker (MSW). Their well-crafted design makes them incredibly easy to use, enhancing the overall testing experience.
How It Works
For those eager to see the actual code, you can find it here: https://github.com/seratch/slack-web-api-client/blob/main/test/retry-handler.test.ts
Here’s a step-by-step guide to setting up a new project and writing effective tests:
Setting Up New Project:
Let's start by creating a new project and install the required dependences:
mkdir my-test-app
cd my-test-app
npm init -y
npm i slack-web-api-client
npm i --save-dev typescript vitest msw
Configuring TypeScript:
Add a basic tsconfig.json
for writing in TypeScript (note that you don't need to use exactly the same one):
{
"compilerOptions": {
"outDir": "./dist",
"target": "es2021",
"noImplicitAny": true,
"module": "commonjs",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"allowJs": false
},
"include": ["src/**/*"]
}
Start Writing Test Code:
Start by setting up Vist and MSW in a new test source file under the ./test
directory:
import { setupServer } from "msw/node";
import { HttpResponse, http } from "msw";
import { afterAll, afterEach, beforeAll, describe, test, expect } from "vitest";
const server = setupServer();
beforeAll(() => server.listen({ onUnhandledRequest: "error" }));
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
Just by including these lines of code, you're ready to capture all outgoing HTTP requests via the fetch
function and reproduce any scenario you'd like!
Now, let's add our first simple test:
import { SlackAPIClient } from "slack-web-api-client";
describe("Slack API client", async () => {
test("can perform api.test API call", async () => {
server.use(
http.post("https://slack.com/api/api.test", () => {
return HttpResponse.json({ ok: true });
}),
);
const client = new SlackAPIClient();
const response = await client.api.test();
expect(response.ok).true;
});
});
Run this test using npx vitest
and check the output. If you see the following output on your terminal, congratulations! You've successfully run your first test using MSW!
$ npx vitest
DEV v1.5.1 /new-app
✓ test/sample.test.ts (1)
✓ Slack API client (1)
✓ can perform api.test API call
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 17:18:37
Duration 653ms (transform 87ms, setup 0ms, collect 256ms, tests 39ms, environment 0ms, prepare 101ms)
PASS Waiting for file changes...
press h to show help, press q to quit
When you modify the server.use(...)
section as shown below,
server.use(
http.post("https://slack.com/api/api.test", () => {
return HttpResponse.text("ratelimited", { status: 429, headers: { "Retry-After": "1" } });
}),
);
the same test should then start to fail:
❯ test/sample.test.ts (1) 1048ms
❯ Slack API client (1) 1047ms
× can perform api.test API call 1045ms
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL test/sample.test.ts > Slack API client > can perform api.test API call
SlackAPIConnectionError: Failed to call api.test (cause: SlackAPIConnectionError: Failed to call api.test (status: 429, body: "ratelimited"))
❯ SlackAPIClient.call node_modules/slack-web-api-client/src/client/api-client.ts:581:21
❯ test/sample.test.ts:24:22
22| );
23| const client = new SlackAPIClient();
24| const response = await client.api.test();
| ^
25| expect(response.ok).true;
26| });
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { apiName: 'api.test', status: -1, body: '', headers: undefined }
Caused by: SlackAPIConnectionError: Failed to call api.test (status: 429, body: "ratelimited")
❯ SlackAPIClient.call node_modules/slack-web-api-client/src/client/api-client.ts:602:13
❯ SlackAPIClient.call node_modules/slack-web-api-client/src/client/api-client.ts:577:18
❯ test/sample.test.ts:24:22
However, this library attempts a retry when it receives a rate-limited error response from Slack. Therefore, after adjusting the scenario to be more realistic,
const responses = [
HttpResponse.text("ratelimited", { status: 429, headers: { "Retry-After": "1" } }),
HttpResponse.json({ ok: true }),
];
server.use(
http.post("https://slack.com/api/api.test", () => {
return responses.shift();
}),
);
or using one-time handlers works well too:
server.use(
http.post("https://slack.com/api/api.test", () => HttpResponse.text("ratelimited", { status: 429, headers: { "Retry-After": "1" } }), { once: true }),
http.post("https://slack.com/api/api.test", () => HttpResponse.json({ ok: true })),
);
the test will start passing again!
RERUN test/sample.test.ts x3
✓ test/sample.test.ts (1) 1044ms
✓ Slack API client (1) 1042ms
✓ can perform api.test API call 1041ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 17:28:21
Duration 1.21s
PASS Waiting for file changes...
The interaction here is very smooth. Every time you save a change to the test code, the test is immediately executed again. Additionally, the outputs from the Vitest framework are so easy to understand that you won't be confused about what to do next.
Wrap Up
For me, using Vitest and MSW has significantly changed the testing experience for SDK development. I highly recommend trying these tools!
Top comments (3)
Thanks for writing this piece!
Please note that MSW has a concept of one-time handlers. Give those a try to emulate that rate limited -> successful response flow.
You can also use generators to track the number of times the same resolver is being hit. We showcase that usage for Polling but it's applicable for anything in general.
Glad you like MSW and hope it makes your developer's life a bit easier!
Oh, wow! Thanks for sharing this. The one-time handlers seem to be exactly what I needed for this use case.
My pleasure! Let me know if you or the team has any questions in how to improve your MSW setup.
I also have a sponsorship tier that gives your company a monthly 1h-long consulting session with me, if you'd like. You can learn more on the GitHub Sponsors profile.