msw-request-assertions
Version:
Request assertions for MSW
425 lines (311 loc) • 11.1 kB
Markdown
# MSW Request Assertions
<div align="center">
A powerful testing library that provides custom assertion matchers for [Mock Service Worker (MSW)](https://mswjs.io/). Test your API interactions with confidence by asserting on request properties like headers, body, query parameters, and more.
  [](https://conventionalcommits.org) [](https://www.typescriptlang.org/)
[Features](#-features) | [Installation](#-installation) | [API](#-api) | [Contributing](#-contributing)
</div>
> [!IMPORTANT]
> **Before using this library**, please read MSW's official guidance on [avoiding request assertions](https://mswjs.io/docs/best-practices/avoid-request-assertions). The MSW team recommends testing **how your application reacts** to requests rather than asserting on request details. This library should primarily be used for edge cases, like request details have no observable application behavior.
## ✨ Features
- ✅ Assert on any aspect of HTTP and GraphQL requests
- ✅ Works with Vitest and Jest
## 📦 Installation
```bash
npm install --save-dev msw-request-assertions msw
# or
yarn add --dev msw-request-assertions msw
# or
pnpm add --save-dev msw-request-assertions msw
```
## 🚀 Quick Start
### Setup for Vitest
```typescript
// vitest.setup.ts
import 'msw-request-assertions/vitest'
```
```typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
setupFiles: ['./vitest.setup.ts'],
},
})
```
```json
// tsconfig.json
{
"compilerOptions": {
// ...rest of your options
"types": ["vitest/globals", "msw-request-assertions/vitest"]
}
}
```
### Setup for Jest
> [!NOTE]
> Jest support is not a first-class citizen due to complex setup requirements between ESM and CJS modules. While we provide Jest compatibility, our main development and testing efforts are focused on Vitest. Feel free to [raise an issue](https://github.com/marklai1998/msw-request-assertions/issues) if you encounter Jest-specific problems.
```typescript
// jest.setup.ts
import 'msw-request-assertions/jest'
```
```javascript
// jest.config.js
module.exports = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
}
```
```json
// tsconfig.json
{
"compilerOptions": {
// ...rest of your options
"types": ["vitest/globals", "msw-request-assertions/jest"]
}
}
```
### Basic Example
```typescript
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
// Create MSW handlers
const userHandler = http.post('/api/users', () => {
return HttpResponse.json({ id: 1, name: 'John Doe' })
})
const server = setupServer(userHandler)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
test('should create user with correct data', async () => {
const userData = { name: 'John Doe', email: 'john@example.com' }
await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
// Assert the request was made
expect(userHandler).toHaveBeenRequested()
// Assert the request body
expect(userHandler).toHaveBeenRequestedWithJsonBody(userData)
// Assert multiple properties at once
expect(userHandler).toHaveBeenRequestedWith({
jsonBody: userData,
headers: { 'content-type': 'application/json' }
})
})
```
## 💻 API
- [Basic Request Matchers](#basic-request-matchers)
- [toHaveBeenRequested](#tohavebeenrequested)
- [toHaveBeenRequestedTimes](#tohavebeenrequestedtimes)
- [Body Matchers](#body-matchers)
- [toHaveBeenRequestedWithBody](#tohavebeenrequestedwithbody)
- [toHaveBeenRequestedWithJsonBody](#tohavebeenrequestedwithjsonbody)
- [Header Matchers](#header-matchers)
- [toHaveBeenRequestedWithHeaders](#tohavebeenrequestedwithheaders)
- [URL Matchers](#url-matchers)
- [toHaveBeenRequestedWithQueryString](#tohavebeenrequestedwithquerystring)
- [toHaveBeenRequestedWithHash](#tohavebeenrequestedwithhash)
- [toHaveBeenRequestedWithPathParameters](#tohavebeenrequestedwithpathparameters)
- [GraphQL Matchers](#graphql-matchers)
- [toHaveBeenRequestedWithGqlQuery](#tohavebeenrequestedwithgqlquery)
- [toHaveBeenRequestedWithGqlVariables](#tohavebeenrequestedwithgqlvariables)
- [Unified Matcher](#unified-matcher)
- [toHaveBeenRequestedWith](#tohavebeenrequestedwith)
- [Nth Call Matchers](#nth-call-matchers)
- [Types](#types)
- [RequestPayload](#requestpayload)
### Basic Request Matchers
#### toHaveBeenRequested
Assert that a handler was called at least once.
```typescript
expect(handler).toHaveBeenRequested()
expect(handler).not.toHaveBeenRequested()
```
#### toHaveBeenRequestedTimes
Assert that a handler was called exactly n times.
```typescript
expect(handler).toHaveBeenRequestedTimes(3)
expect(handler).not.toHaveBeenRequestedTimes(1)
```
### Body Matchers
#### toHaveBeenRequestedWithBody
Assert on raw request body (string).
```typescript
expect(handler).toHaveBeenRequestedWithBody('{"name":"John Doe","email":"john@example.com"}')
// Nth call variant
expect(handler).toHaveBeenNthRequestedWithBody(1, 'first call body')
```
#### toHaveBeenRequestedWithJsonBody
Assert on JSON request body (parsed object).
```typescript
expect(handler).toHaveBeenRequestedWithJsonBody({
name: 'John Doe',
email: 'john@example.com'
})
// Nth call variant
expect(handler).toHaveBeenNthRequestedWithJsonBody(2, { userId: '123' })
```
### Header Matchers
#### toHaveBeenRequestedWithHeaders
Assert on request headers. Headers are case-insensitive.
```typescript
expect(handler).toHaveBeenRequestedWithHeaders({
'authorization': 'Bearer token123',
'content-type': 'application/json'
})
// Nth call variant
expect(handler).toHaveBeenNthRequestedWithHeaders(1, { 'x-api-key': 'secret' })
```
### URL Matchers
#### toHaveBeenRequestedWithQueryString
Assert on URL query parameters.
> [!NOTE]
> The input is a string to avoid serialization issues, especially with arrays and custom formats. This gives you full control over the exact query string format.
```typescript
expect(handler).toHaveBeenRequestedWithQueryString('?page=1&limit=10')
expect(handler).toHaveBeenRequestedWithQueryString('') // No query params
// Nth call variant
expect(handler).toHaveBeenNthRequestedWithQueryString(2, '?filter=active')
// Custom serialization with qs library
import qs from 'qs'
const params = {
filters: ['active', 'verified'],
sort: { field: 'name', order: 'asc' },
page: 1
}
// Different serialization formats
const standardFormat = '?' + new URLSearchParams({
'filters[]': 'active',
'filters[]': 'verified'
}).toString()
const qsFormat = '?' + qs.stringify(params, {
arrayFormat: 'brackets',
encode: false
})
// Results in: ?filters[]=active&filters[]=verified&sort[field]=name&sort[order]=asc&page=1
expect(handler).toHaveBeenRequestedWithQueryString(qsFormat)
```
#### toHaveBeenRequestedWithHash
Assert on URL hash fragment.
```typescript
expect(handler).toHaveBeenRequestedWithHash('#section1')
// Nth call variant
expect(handler).toHaveBeenNthRequestedWithHash(1, '#top')
```
#### toHaveBeenRequestedWithPathParameters
Assert on URL path parameters (for dynamic routes).
```typescript
// For route: /users/:userId/posts/:postId
expect(handler).toHaveBeenRequestedWithPathParameters({
userId: '123',
postId: '456'
})
// Nth call variant
expect(handler).toHaveBeenNthRequestedWithPathParameters(1, { id: '789' })
```
### GraphQL Matchers
#### toHaveBeenRequestedWithGqlQuery
Assert on GraphQL query string.
```typescript
expect(gqlHandler).toHaveBeenRequestedWithGqlQuery(`
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`)
// Nth call variant
expect(gqlHandler).toHaveBeenNthRequestedWithGqlQuery(1, 'query { users { id } }')
```
#### toHaveBeenRequestedWithGqlVariables
Assert on GraphQL variables.
```typescript
expect(gqlHandler).toHaveBeenRequestedWithGqlVariables({
id: '123',
filters: { status: 'active' }
})
// Nth call variant
expect(gqlHandler).toHaveBeenNthRequestedWithGqlVariables(2, { userId: '456' })
```
### Unified Matcher
#### toHaveBeenRequestedWith
Assert on multiple request properties at once. See [RequestPayload](#requestpayload) for all available properties.
```typescript
// HTTP example
expect(handler).toHaveBeenRequestedWith({
jsonBody: { name: 'John' },
headers: { 'authorization': 'Bearer token' },
queryString: '?page=1',
hash: '#top',
pathParameters: { userId: '123' }
})
// GraphQL example
expect(gqlHandler).toHaveBeenRequestedWith({
gqlQuery: 'query GetUser($id: ID!) { user(id: $id) { name } }',
gqlVariables: { id: '123' }
})
// Nth call variant
expect(handler).toHaveBeenNthRequestedWith(2, {
jsonBody: { action: 'update' },
headers: { 'content-type': 'application/json' }
})
```
### Nth Call Matchers
All matchers have an "nth" variant to assert on specific call positions. The first argument is the call number (1-indexed).
```typescript
// Basic matchers
expect(handler).toHaveBeenNthRequestedWithBody(1, 'first call body')
expect(handler).toHaveBeenNthRequestedWithJsonBody(2, { data: 'second call' })
expect(handler).toHaveBeenNthRequestedWithHeaders(3, { 'x-retry': '2' })
// URL matchers
expect(handler).toHaveBeenNthRequestedWithQueryString(1, '?page=1')
expect(handler).toHaveBeenNthRequestedWithHash(2, '#section2')
expect(handler).toHaveBeenNthRequestedWithPathParameters(1, { id: '123' })
// GraphQL matchers
expect(gqlHandler).toHaveBeenNthRequestedWithGqlQuery(1, 'query { users }')
expect(gqlHandler).toHaveBeenNthRequestedWithGqlVariables(2, { limit: 10 })
// Unified matcher
expect(handler).toHaveBeenNthRequestedWith(3, {
jsonBody: { name: 'Jane' },
queryString: '?include=profile'
})
```
### Types
#### RequestPayload
The payload object used with `toHaveBeenRequestedWith` matcher.
```typescript
type RequestPayload = {
// HTTP/GraphQL body content
body?: string;
jsonBody?: unknown;
// HTTP headers (case-insensitive)
headers?: Record<string, string>;
// URL components
queryString?: string;
hash?: string;
pathParameters?: Record<string, string>;
// GraphQL specific
gqlQuery?: string;
gqlVariables?: unknown;
}
```
## 🤝 Contributing
### Development
#### Local Development
```bash
pnpm i
pnpm test
```
#### Build
```bash
pnpm build
```
### Release
This repo uses [Release Please](https://github.com/google-github-actions/release-please-action) to release.
#### To release a new version
1. Merge your changes into the `main` branch.
2. An automated GitHub Action will run, triggering the creation of a Release PR.
3. Merge the release PR.
4. Wait for the second GitHub Action to run automatically.
5. Congratulations, you're all set!