Skip to content

Instantly share code, notes, and snippets.

@pachun
Created May 26, 2025 09:47
Show Gist options
  • Save pachun/b6c66fe30db4f38f4bbbf4ea01de5d82 to your computer and use it in GitHub Desktop.
Save pachun/b6c66fe30db4f38f4bbbf4ea01de5d82 to your computer and use it in GitHub Desktop.
Replace nock() with mswNock() (also, uninstall nock and install msw in your development dependencies)
import { http, HttpResponse, HttpHandler, StrictRequest } from "msw"
import { setupServer } from "msw/native"
const server = setupServer()
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
type Headers = Record<string, string>
type QueryParams = Record<string, string | null>
type HttpMethod = "get" | "post" | "put" | "delete"
export class MSWNock {
private method: HttpMethod = "get"
private fullUrl = ""
private expectedBody?: Record<string, any>
private expectedHeaders: Headers = {}
private expectedQuery?: QueryParams
private isDoneInternal = false
private hasDelayedResponse?: boolean = false
constructor(private baseUrl: string) {}
get(path: string) {
this.method = "get"
this.fullUrl = this.baseUrl + path
return this
}
post(path: string, body?: Record<string, any>) {
this.method = "post"
this.fullUrl = this.baseUrl + path
this.expectedBody = body
return this
}
put(path: string, body?: Record<string, any>) {
this.method = "put"
this.fullUrl = this.baseUrl + path
this.expectedBody = body
return this
}
delete(path: string) {
this.method = "delete"
this.fullUrl = this.baseUrl + path
return this
}
delay(hasDelayedResponse: boolean) {
this.hasDelayedResponse = hasDelayedResponse
return this
}
matchHeader(key: string, value: string) {
this.expectedHeaders[key.toLowerCase()] = value
return this
}
query(params: QueryParams) {
this.expectedQuery = params
return this
}
replyWithError() {
this.setupHandler(null, true)
return this
}
reply(status: number, responseBody: any = {}) {
this.setupHandler(() => responseBody, false, status)
return this
}
isDone() {
return this.isDoneInternal
}
private setupHandler(
getResponseBody: (() => any) | null,
isError: boolean,
status = 200,
) {
const handler: HttpHandler = http[this.method](
this.fullUrl,
async ({ request }) => {
this.isDoneInternal = true
const url = new URL(request.url)
this.validateHeaders(request)
await this.validateJsonBody(request)
this.validateQuery(url)
if (this.hasDelayedResponse) {
await new Promise(resolve => setImmediate(resolve))
}
if (isError) {
throw new Error("Simulated network error from mswNock.replyWithError")
}
const body = getResponseBody!()
return HttpResponse.json(body, { status })
},
)
server.use(handler)
}
private validateHeaders(request: StrictRequest<any>) {
const reqHeaders = Object.fromEntries(request.headers.entries())
for (const [key, value] of Object.entries(this.expectedHeaders)) {
if (reqHeaders[key.toLowerCase()] !== value) {
this.failJestTest(
`Expected header "${key}" to be "${value}", but got "${reqHeaders[key.toLowerCase()]}"`,
)
}
}
}
private async validateJsonBody(request: StrictRequest<any>) {
const contentType = request.headers.get("content-type") ?? ""
if (this.expectedBody && contentType.includes("application/json")) {
const body = await request.json()
for (const [key, expectedValue] of Object.entries(this.expectedBody)) {
if (body[key] !== expectedValue) {
this.failJestTest(
`Expected request body key "${key}" to be "${expectedValue}", but got "${body[key]}"`,
)
}
}
}
}
private validateQuery(url: URL) {
if (!this.expectedQuery) return
for (const [key, expectedValue] of Object.entries(this.expectedQuery)) {
const actual = url.searchParams.get(key)
if (actual !== expectedValue) {
this.failJestTest(
`Expected query param "${key}"="${expectedValue}", but got "${actual}"`,
)
}
}
}
private failJestTest(message: string) {
it(message, () => {})
}
}
export const mswNock = (baseUrl: string) => new MSWNock(baseUrl)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment