使用 Mock Service Worker 和 TypeScript 实现类型安全的 API 模拟
REST API
GraphQL API
高级用法
注意:本文介绍的是 MSW v1 版本。我们已发布MSW v2版本,该版本在类型安全方面进行了改进,并修复了一些错误。有关 MSW v2 中类型安全的 API 模拟,请参阅“与 TypeScript 一起使用”页面。谢谢!
Mock Service Worker是一个适用于浏览器和 Node.js 的无缝 API 模拟库。它使用 Service Worker API 在网络层拦截请求,这意味着无需再对“fetch”、“axios”或任何其他发出请求的客户端进行桩化。它在模拟 REST 和 GraphQL API 时提供一流的体验,并允许您在测试、开发和调试过程中复用相同的模拟对象。
观看这段 4 分钟的教程,了解如何使用 Mock Service Worker 模拟基本的 REST API 响应,从而更好地理解该库的工作原理和使用体验:
今天我们将深入探讨如何将 TypeScript 添加到您的 API 模拟体验中,使其更进一步。
为什么要给模拟对象添加注释?
你编写的模拟对象就像其他逻辑代码一样,是应用程序的一部分。进行类型验证是确保模拟对象满足数据预期的最经济高效的方法之一。
REST API
每个REST 请求处理程序都具有以下类型签名:
type RestHandler = <RequestBody, ResponseBody, RequestParams>(mask, resolver) => MockedResponse
这样我们就可以在 REST API 处理程序中添加三项注释:
- 请求体类型。
- 回复主体类型。
- 请求参数。
让我们来看一下UPDATE /post/:postId使用了上述三个泛型的请求:
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, viewsCount } = req.body
return res(
ctx.json({
updatedAt: Date.now()
})
)
})
相同的泛型适用于任何
rest请求处理程序:rest.get(),,,等等rest.post()。rest.delete()
GraphQL API
GraphQL 处理程序的类型签名是:
type GraphQLHandler = <Query, Variables>(args) => MockedResponse
这意味着我们可以对查询的Query类型(响应中返回的内容)和值进行注释Variables。
让我们来看一些具体的例子。
GraphQL 查询
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突变
现在,让我们将相同的方法应用于 GraphQL mutation。在下面的示例中,我们有一个UpdateArticlemutation,它会根据文章 ID 更新文章。
import { graphql } from 'msw'
interface UpdateArticleMutation {
article: {
title: "string"
updatedAt: Date
}
}
interface UpdateArticleMutationVariables {
title: "string"
}
graphql.mutation
<UpdateArticleMutation, UpdateArticleMutationVariables>(
'UpdateArticle',
(req, res, ctx) => {
const { title } = req.variables
return res(
ctx.data({
article: {
title,
updatedAt: Date.now()
}
})
)
})
GraphQL 操作
当需要捕获各种类型/名称的多个 GraphQL 操作时,它graphql.operation()的优势就显现出来了。虽然传入查询的性质变得难以预测,但您仍然可以使用处理程序的泛型来指定其类型Query。Variables
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)))
})
额外功能:与 GraphQL 代码生成器配合使用
我最喜欢的 GraphQL API 模拟方案是将GraphQL 代码生成器添加到其中。
GraphQL 代码生成器是一个非常棒的工具,它不仅可以从 GraphQL schema 生成类型定义,还可以从应用程序执行的确切查询/变更生成类型定义。
以下示例展示了如何将 GraphQL Codegen 生成的类型集成到请求处理程序中:
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) => {})
由于您的数据成为请求处理程序的权威来源,您可以始终确信您的模拟结果反映了应用程序的实际行为。此外,您也无需手动注释查询,这大大节省了时间!
高级用法
上面我们已经介绍了大多数常见的用法示例,接下来让我们讨论一下当你抽象、重构和自定义模拟设置时的情况。
自定义响应解析器
将响应解析器逻辑隔离到高阶函数中并不罕见,这样可以防止重复,同时还能控制模拟响应。
以下是如何为自定义响应解析器添加注解的方法:
// src/mocks/resolvers.ts
import type { ResponseResolver, RestRequest, RestContext } from 'msw'
interface User {
firstName: string
lastName: string
}
export const userResolver = (user: User | User[]): ResponseResolver<RestRequest, RestContext, User> => {
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])
自定义响应转换器
您可以在响应转换器之上创建自定义上下文实用程序。
以下示例展示了如何创建一个自定义响应转换器,该转换器使用该json-bigint库来支持模拟响应的 JSON 正文中的 BigInt 类型。
// 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))
)
}
请注意如何利用从 MSW 导出的
compose内容来构建自定义响应转换器的逻辑。context
jsonBigInt在处理程序中编写模拟响应时,可以使用该转换器:
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
})
)
})
后记
希望这篇文章对您有所帮助,并能让您学到一些关于如何通过类型定义(无论是手动定义还是自动生成的定义)来改进模拟对象的知识。
在其他情况下,您可能也需要使用类型来覆盖您的模拟对象。请探索 MSW 导出的类型定义,并参考库的实现。
请将这篇文章分享给你的同事,并在推特上转发,非常感谢!