Easier TypeScript API Testing with Vitest + MSW

RMAG news

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.error).toBeUndefined();
});
});

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.json(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| assert.isUndefined(response.error);
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.json(ratelimited, { status: 429, headers: { Retry-After: 1 } }),
HttpResponse.json({ ok: true }),
];
server.use(
http.post(https://slack.com/api/api.test, () => {
return responses.shift();
}),
);

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!

Leave a Reply

Your email address will not be published. Required fields are marked *