This content originally appeared on DEV Community and was authored by Artem Zakharchenko
Mock Service Worker is a seamless API mocking library for browser and Node.js. It uses Service Worker API to intercept requests on the network level, meaning no more stubbing of "fetch", "axios", or any other request issuing client. It provides a first-class experience when mocking REST and GraphQL API, and allows you to reuse the same mocks for testing, development, and debugging.
Watch this 4 minutes tutorial on mocking a basic REST API response with Mock Service Worker to get a better understanding of how this library works and feels:
Today we're going to have a practical dive-in into adding TypeScript to your API mocking experience to bring it one step further.
Why annotate mocks?
The mocks you write are a part of your application like any other piece of logic. Having a type validation is one of the cheapest and most efficient ways to ensure your mocks satisfy the data expectations towards them.
REST API
Each REST request handler has the following type signature:
type RestHandler = <RequestBody, ResponseBody, RequestParams>(mask, resolver) => MockedResponse
This allows us to annotate three things in our REST API handlers:
- Request body type.
- Response body type.
- Request parameters.
Let's take a look at the UPDATE /post/:postId
request that utilizes all three said generics:
import { rest } from 'msw'
// Describe the shape of the "req.body".
interface UpdatePostRequestBody {
title: "string"
viewsCount: string
}
// Describe the shape of the mocked response body.
interface UpdatePostResponseBody {
updatedAt: Date
}
// Describe the shape of the "req.params".
interface UpdatePostRequestParams {
postId: string
}
rest.update
<UpdatePostRequestBody, UpdatePostResponseBody, UpdatePostRequestParams>(
'/post/:postId',
(req, res, ctx) => {
const { postId } = req.params
const { title, viesCount } = req.body
return res(
ctx.json({
updatedAt: Date.now()
})
)
})
The same generics apply to any
rest
request handler:rest.get()
,rest.post()
,rest.delete()
, etc.
GraphQL API
A type signature for the GraphQL handlers is:
type GraphQLHandler = <Query, Variables>(args) => MockedResponse
This means we can annotate the Query
type (what gets returned in the response) and the Variables
of our query.
Let's take a look at some concrete examples.
GraphQL queries
import { graphql } from 'msw'
// Describe the payload returned via "ctx.data".
interface GetUserQuery {
user: {
id: string
firstName: string
lastName: string
}
}
// Describe the shape of the "req.variables" object.
interface GetUserQueryVariables {
userId: string
}
graphql.query
<GetUserQuery, GetUserQueryVariables>(
'GetUser',
(req, res, ctx) => {
const { userId } = req.variables
return res(
ctx.data({
user: {
id: userId,
firstName: 'John',
lastName: 'Maverick'
}
})
)
})
GraphQL mutations
Now, let's apply the same approach to a GraphQL mutation. In the case below we're having a UpdateArticle
mutation that updates an article by its ID.
import { graphql } from 'msw'
interface UpdateArticleMutation {
article: {
title: "string"
updatedAt: Date
}
}
interface UpdateArticleMutationVariables {
title: "string"
}
graphql.mutation
<EditArticleMutation, EditArticleMutationVariables>(
'UpdateArticle',
(req, res, ctx) => {
const { title } = req.variables
return res(
ctx.data({
article: {
title,
updatedAt: Date.now()
}
})
)
})
GraphQL operations
When it comes to capturing multiple GraphQL operations regardless of their kind/name, the graphql.operation()
truly shines. Although the nature of the incoming queries becomes less predictable, you can still specify its Query
and Variables
types using the handler's generics.
import { graphql } from 'msw'
type Query =
| { user: { id: string } }
| { article: { updateAt: Date } }
| { checkout: { item: { price: number } } }
type Variables =
| { userId: string }
| { articleId: string }
| { cartId: string }
graphql.operation<Query, Variables>((req, res, ctx) => {
// In this example we're calling an abstract
// "resolveOperation" function that returns
// the right query payload based on the request.
return res(ctx.data(resolveOperation(req)))
})
Bonus: Using with GraphQL Code Generator
My absolute favorite setup for mocking GraphQL API is when you add GraphQL Code Generator to the mix.
GraphQL Code Generator is a superb tool that allows you to generate type definitions from your GraphQL schema, but also from the exact queries/mutations your application makes.
Here's an example of how to integrate the types generated by GraphQL Codegen into your request handlers:
import { graphql } from 'msw'
// Import types generated from our GraphQL schema and queries.
import { GetUserQuery, GetUserQueryVariables } from './types'
// Annotate request handlers to match
// the actual behavior of your application.
graphql.query<GetUserQuery, GetUserQueryVariables>('GetUser', (req, res, ctx) => {})
With your data becoming the source of truth for your request handlers, you're always confident that your mocks reflect the actual behavior of your application. You also remove the need to annotate queries manually, which is a tremendous time-saver!
Advanced usage
We've covered most of the common usage examples above, so let's talk about those cases when you abstract, restructure and customize your mocking setup.
Custom response resolvers
It's not uncommon to isolate a response resolver logic into a higher-order function to prevent repetition while remaining in control over the mocked responses.
This is how you'd annotate a custom response resolver:
// src/mocks/resolvers.ts
import { ResponseResolver } from 'msw'
interface User {
firstName: string
lastName: string
}
export const userResolver = (user: User | User[]): ResponseResolver => {
return (req, res, ctx) => {
return res(ctx.json(user)
}
})
import { rest } from 'msw'
import { userResolver } from './resolvers'
import { commonUser, adminUser } from './fixtures'
rest.get('/user/:userId', userResolver(commonUser))
rest.get('/users', userResolver([commonUser, adminUser])
Custom response transformers
You can create custom context utilities on top of response transformers.
Here's an example of how to create a custom response transformer that uses the json-bigint
library to support BigInt in the JSON body of your mocked responses.
// src/mocks/transformers.ts
import * as JsonBigInt from 'json-bigint'
import { ResponseTransformer, context, compose } from 'msw'
// Here we're creating a custom context utility
// that can handle a BigInt values in JSON.
export const jsonBigInt =
(body: Record<string, any>): ResponseTransformer => {
return compose(
context.set('Content-Type', 'application/hal+json'),
context.body(JsonBigInt.stringify(body))
)
}
Note how you can compose your custom response transformer's logic by utilizing the
compose
andcontext
exported from MSW.
You can use that jsonBigInt
transformer when composing mocked responses in your handlers:
import { rest } from 'msw'
import { jsonBigInt } from './transformers'
rest.get('/stats', (req, res, ctx) => {
return res(
// Use the custom context utility the same way
// you'd use the default ones (i.e. "ctx.json()").
jsonBigInt({
username: 'john.maverick',
balance: 1597928668063727616
})
)
})
Afterword
Hope you find this article useful and learn a thing or two about improving your mocks by covering them with type definitions—either manual or generated ones.
There can be other scenarios when you may find yourself in need to cover your mocks with types. Explore what type definitions MSW exports and take a look at the library's implementation for reference.
Share this article with your colleagues and give it a shoutout on Twitter, I'd highly appreciate it! Thank you.
Useful resources
This content originally appeared on DEV Community and was authored by Artem Zakharchenko
Artem Zakharchenko | Sciencx (2021-02-28T14:10:14+00:00) Type-safe API mocking with Mock Service Worker and TypeScript. Retrieved from https://www.scien.cx/2021/02/28/type-safe-api-mocking-with-mock-service-worker-and-typescript/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.