惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
Lohrmann on Cybersecurity
Martin Fowler
Martin Fowler
Engineering at Meta
Engineering at Meta
腾讯CDC
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Microsoft Azure Blog
Microsoft Azure Blog
G
Google Developers Blog
TaoSecurity Blog
TaoSecurity Blog
博客园_首页
Vercel News
Vercel News
Hugging Face - Blog
Hugging Face - Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Last Week in AI
Last Week in AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
The Exploit Database - CXSecurity.com
量子位
Project Zero
Project Zero
A
Arctic Wolf
小众软件
小众软件
NISL@THU
NISL@THU
C
CERT Recently Published Vulnerability Notes
有赞技术团队
有赞技术团队
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
N
News and Events Feed by Topic
宝玉的分享
宝玉的分享
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Troy Hunt's Blog
P
Privacy & Cybersecurity Law Blog
Security Latest
Security Latest
B
Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
D
DataBreaches.Net
Schneier on Security
Schneier on Security
The Hacker News
The Hacker News
K
Kaspersky official blog
C
Check Point Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Webroot Blog
Webroot Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
人人都是产品经理
人人都是产品经理
AI
AI
Cisco Talos Blog
Cisco Talos Blog
MyScale Blog
MyScale Blog
Cloudbric
Cloudbric
B
Blog RSS Feed
S
Schneier on Security
P
Palo Alto Networks Blog

Kent C. Dodds Blog

Implementing Hybrid Semantic + Lexical Search Simplifying Containers with Cloudflare Sandboxes Migrating to Workspaces and Nx Offloading FFmpeg with Cloudflare Building Semantic Search on my Content Helping YOU ask ME questions with AI How I used Cursor to Migrate Frameworks The Dow's Start on the Covenant Path 2025 in Review The next chapter: EpicAI.pro AI is taking your job How I increased my visibility Launching Epic Web 2023 in Review Stop Being a Junior RSC with Dan Abramov and Joe Savona Live Stream Fixing a Memory Leak in a Production Node.js App 2022 in Review My Car Accident I Migrated from a Postgres Cluster to Distributed SQLite with LiteFS I'm building EpicWeb.dev A review of my time at Remix Remix: The Yang to React's Yin How I help you build better websites Why I Love Remix The State Initializer Pattern How to React ⚛️ Get a catch block error message with TypeScript Building an awesome image loading experience How Remix makes CSS clashes predictable Introducing the new kentcdodds.com How I built a modern website in 2021 How to use React Context effectively Static vs Unit vs Integration vs E2E Testing for Frontend Apps The Testing Trophy and Testing Classifications Array reduce vs chaining vs for loop Don't Solve Problems, Eliminate Them Super Simple Start to Remix Super Simple Start to ESModules in Node.js JavaScript Pass By Value Function Parameters How to write a Constrained Identity Function (CIF) in TypeScript How to optimize your context value How to write a React Component in TypeScript TypeScript Function Syntaxes Listify a JavaScript Array Build vs Buy: Component Libraries edition Using fetch with TypeScript Wrapping React.useState with TypeScript Define function overload types with TypeScript 2020 in Review Business and Engineering alignment Hi, thanks for reaching out to me 👋 useEffect vs useLayoutEffect Super simple start to Firebase functions Super simple start to Netlify functions Super Simple Start to css variables Favor Progress Over Pride in Open Source Testing Implementation Details How getting into Open Source has been awesome for me useState lazy initialization and function updates Use ternaries rather than && in JSX Application State Management with React Use react-error-boundary to handle errors in React JavaScript to Know for React How I structure Express apps What open source project should I contribute to? When I follow TDD AHA Programming 💡 How I Record Educational Videos Should I write a test or fix a bug? Intentional Career Building Improve test error messages of your abstractions Tracing user interactions with React Eliminate an entire category of bugs with a few simple tools Common mistakes with React Testing Library Super Simple Start to React Stop using client-side route redirects The State Reducer Pattern with React Hooks Function forms Replace axios with a simple custom fetch wrapper How to test custom React hooks React Production Performance Monitoring Should I useState or useReducer? Stop using isLoading booleans Make Your Test Fail Make your own DevTools An Argument for Automation Fix the "not wrapped in act(...)" warning Super Simple Start to ESModules in the Browser Implementing a simple state machine library in JavaScript 2010s Decade in Review Why users care about how you write code Why I avoid nesting closures Don't call a React function component Why your team needs TestingJavaScript.com Inversion of Control Understanding React's key prop How to Enable React Concurrent Mode How to add testing to an existing project Profile a React App for Performance
Stop mocking fetch
Kent C. Dodds 🏹 @kentcdodds · 2020-06-03 · via Kent C. Dodds Blog

What's wrong with this test?

// __tests__/checkout.js
import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { client } from '#app/utils/api-client'

jest.mock('#app/utils/api-client')

test('clicking "confirm" submits payment', async () => {
	const shoppingCart = buildShoppingCart()
	render(<Checkout shoppingCart={shoppingCart} />)

	client.mockResolvedValueOnce(() => ({ success: true }))

	userEvent.click(screen.getByRole('button', { name: /confirm/i }))

	expect(client).toHaveBeenCalledWith('checkout', { data: shoppingCart })
	expect(client).toHaveBeenCalledTimes(1)
	expect(await screen.findByText(/success/i)).toBeInTheDocument()
})

This is a bit of a trick question. Without knowing the actual API and requirements of Checkout as well as the /checkout endpoint, you can't really answer. So, sorry about that. But, one issue with this is because you're mocking out the client, how do you really know the client is being used correctly in this case? Sure, the client could be unit tested to make sure it's calling window.fetch properly, but how do you know that client didn't just recently change its API to accept a body instead of data? Oh, you're using TypeScript so you've eliminated a category of bugs. Good! But there are definitely some business logic bugs that can slip in because we're mocking the client here. Sure you could lean on your E2E tests to give you that confidence, but wouldn't it be better to just call into the client and get that confidence here at this lower level where you have a tighter feedback loop? If it's not much more difficult then sure!

But we don't want to actually make fetch requests right? So let's mock out window.fetch:

// __tests__/checkout.js
import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

beforeAll(() => jest.spyOn(window, 'fetch'))
// assuming jest's resetMocks is configured to "true" so
// we don't need to worry about cleanup
// this also assumes that you've loaded a fetch polyfill like `whatwg-fetch`

test('clicking "confirm" submits payment', async () => {
	const shoppingCart = buildShoppingCart()
	render(<Checkout shoppingCart={shoppingCart} />)

	window.fetch.mockResolvedValueOnce({
		ok: true,
		json: async () => ({ success: true }),
	})

	userEvent.click(screen.getByRole('button', { name: /confirm/i }))

	expect(window.fetch).toHaveBeenCalledWith(
		'/checkout',
		expect.objectContaining({
			method: 'POST',
			body: JSON.stringify(shoppingCart),
		}),
	)
	expect(window.fetch).toHaveBeenCalledTimes(1)
	expect(await screen.findByText(/success/i)).toBeInTheDocument()
})

This will give you a bit more confidence that a request is actually being issued, but another thing that this test is lacking is an assertion that the headers has a Content-Type of application/json. Without that, how can you be certain that the server is going to recognize that request you're making? Oh, and how do you ensure that the correct authentication information is being sent along as well?

I hear you, "but we've verified that in our client unit tests, Kent. What more do you want from me!? I don't want to copy/paste assertions everywhere!" I definitely feel you there. But what if there were a way to avoid all the extra work on assertions everywhere, but also get that confidence in every test? Keep reading.

One thing that really bothers me about mocking things like fetch is that you end up re-implementing your entire backend... everywhere in your tests. Often in multiple tests. It's super annoying, especially when it's like: "in this test, we just assume the normal backend responses," but you have to mock those out all over the place. In those cases it's really just setup noise that gets between you and the thing you're actually trying to test.

What inevitably happens is one of these scenarios:

  1. We mock out the client (like in our first test) and rely on the some E2E tests to give us a little confidence that at least the most important parts are using the client correctly. This results in reimplementing our backend anywhere we test things that touch the backend. Often duplicating work.
  2. We mock out window.fetch (like in our second test). This is a little better, but it suffers from some of the same problems as #1.
  3. We put all of our stuff in small functions and unit test it all in isolation (not really a bad thing by itself) and not bother testing them in integration (not a great thing).

Ultimately, we have less confidence, a slower feedback loop, lots of duplicate code, or any combination of those.

One thing that ended up working pretty well for me for a long time is to mock fetch in one function which is basically a re-implementation of all the parts of my backend I have tested. I did a form of this at PayPal and it worked really well. You can think of it like this:

// add this to your setupFilesAfterEnv config in jest so it's imported for every test file
import * as users from './users'

async function mockFetch(url, config) {
	switch (url) {
		case '/login': {
			const user = await users.login(JSON.parse(config.body))
			return {
				ok: true,
				status: 200,
				json: async () => ({ user }),
			}
		}
		case '/checkout': {
			const isAuthorized = user.authorize(config.headers.Authorization)
			if (!isAuthorized) {
				return Promise.reject({
					ok: false,
					status: 401,
					json: async () => ({ message: 'Not authorized' }),
				})
			}
			const shoppingCart = JSON.parse(config.body)
			// do whatever other things you need to do with this shopping cart
			return {
				ok: true,
				status: 200,
				json: async () => ({ success: true }),
			}
		}
		default: {
			throw new Error(`Unhandled request: ${url}`)
		}
	}
}

beforeAll(() => jest.spyOn(window, 'fetch'))
beforeEach(() => window.fetch.mockImplementation(mockFetch))

Now my test can look like this:

// __tests__/checkout.js
import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('clicking "confirm" submits payment', async () => {
	const shoppingCart = buildShoppingCart()
	render(<Checkout shoppingCart={shoppingCart} />)

	userEvent.click(screen.getByRole('button', { name: /confirm/i }))

	expect(await screen.findByText(/success/i)).toBeInTheDocument()
})

My happy-path test doesn't need to do anything special. Maybe I would add a fetch mock for a failure case, but I was pretty happy with this.

What's great about this is I only increase in my confidence and I have even less test code to write for the majority of cases.

Then I discovered msw

msw is short for "Mock Service Worker". Now, service workers don't work in Node, they're a browser feature. However, msw supports Node anyway for testing purposes.

The basic idea is this: create a mock server that intercepts all requests and handle it just like you would if it were a real server. In my own implementation, this means I make a "database" either out of json files to "seed" the database, or "builders" using something like faker or test-data-bot. Then I make server handlers (similar to the express API) and interact with that mock database. This makes my tests fast and easy to write (once you have things set up).

You may have used something like nock to do this sort of thing before. But the cool thing about msw (and something I may write about later), is that you can also use all the exact same "server handlers" in the browser during development as well. This has a few great benefits:

  1. If the endpoint isn't ready
  2. If the endpoint is broken
  3. If your internet connection is slow or non-existent

You might have heard of Mirage which does much of the same thing. However (currently) mirage does not use a service worker in the client and I really like that the network tab works the same whether I have msw installed or not. Learn more about their differences.

Example

So with that intro, here's how we'd do our above example with msw backing our mock server:

// server-handlers.js
// this is put into here so I can share these same handlers between my tests
// as well as my development in the browser. Pretty sweet!
import { rest } from 'msw' // msw supports graphql too!
import * as users from './users'

const handlers = [
	rest.get('/login', async (req, res, ctx) => {
		const user = await users.login(JSON.parse(req.body))
		return res(ctx.json({ user }))
	}),
	rest.post('/checkout', async (req, res, ctx) => {
		const user = await users.login(JSON.parse(req.body))
		const isAuthorized = user.authorize(req.headers.Authorization)
		if (!isAuthorized) {
			return res(ctx.status(401), ctx.json({ message: 'Not authorized' }))
		}
		const shoppingCart = JSON.parse(req.body)
		// do whatever other things you need to do with this shopping cart
		return res(ctx.json({ success: true }))
	}),
]

export { handlers }
// test/server.js
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { handlers } from './server-handlers'

const server = setupServer(...handlers)
export { server, rest }
// test/setup-env.js
// add this to your setupFilesAfterEnv config in jest so it's imported for every test file
import { server } from './server.js'

beforeAll(() => server.listen())
// if you need to add a handler after calling setupServer for some specific test
// this will remove that handler for the rest of them
// (which is important for test isolation):
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

Now my test can look like this:

// __tests__/checkout.js
import * as React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

test('clicking "confirm" submits payment', async () => {
	const shoppingCart = buildShoppingCart()
	render(<Checkout shoppingCart={shoppingCart} />)

	userEvent.click(screen.getByRole('button', { name: /confirm/i }))

	expect(await screen.findByText(/success/i)).toBeInTheDocument()
})

I'm happier with this solution than mocking fetch because:

  1. I don't have to worry about the implementation details of fetch response properties and headers.
  2. If I get something wrong with the way I call fetch, then my server handler won't be called and my test (correctly) fails, which would save me from shipping broken code.
  3. I can reuse these exact same server handlers in my development!

Colocation and error/edge case testing

One reasonable concern about this approach is that you end up putting all of your server handlers in one place and then the tests that rely on those server handlers end up in entirely different files, so you lose the benefits of colocation.

First off, I'd say that you want to only colocate the things that are important and unique to your test. You wouldn't want to have to duplicate all the setup in every test. Only the parts that are unique. So the "happy path" stuff is typically better to just include in your setup file, removed from the test itself. Otherwise you have too much noise and it's hard to isolate what's actually being tested.

But what about edge cases and errors? For those, MSW has the ability for you to add additional server handlers at runtime (within a test) and then reset the server to the original handlers (effectively removing the runtime handlers) to preserve test isolation. Here's an example:

// __tests__/checkout.js
import * as React from 'react'
import { server, rest } from 'test/server'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

// happy path test, no special server stuff
test('clicking "confirm" submits payment', async () => {
	const shoppingCart = buildShoppingCart()
	render(<Checkout shoppingCart={shoppingCart} />)

	userEvent.click(screen.getByRole('button', { name: /confirm/i }))

	expect(await screen.findByText(/success/i)).toBeInTheDocument()
})

// edge/error case, special server stuff
// note that the afterEach(() => server.resetHandlers()) we have in our
// setup file will ensure that the special handler is removed for other tests
test('shows server error if the request fails', async () => {
	const testErrorMessage = 'THIS IS A TEST FAILURE'
	server.use(
		rest.post('/checkout', async (req, res, ctx) => {
			return res(ctx.status(500), ctx.json({ message: testErrorMessage }))
		}),
	)
	const shoppingCart = buildShoppingCart()
	render(<Checkout shoppingCart={shoppingCart} />)

	userEvent.click(screen.getByRole('button', { name: /confirm/i }))

	expect(await screen.findByRole('alert')).toHaveTextContent(testErrorMessage)
})

So you can have colocation where it's needed, and abstraction where abstraction is sensible.

Conclusion

There's definitely more to do with msw, but let's just wrap up for now. If you want to see msw in action, my 4 part workshop "Build React Apps" (included in EpicReact.Dev) uses it and you can find all the material on GitHub.

One really cool aspect of this method of testing is that because you're so far away from implementation details, you can make significant refactorings and your tests can give you confidence that you didn't break the user experience. That's what tests are for!! Love it when this happens:

I completely changed how authentication works in my app the other day and only needed to make a small tweak to my test utils and all my tests (unit, integration, and E2E) passed, giving me confidence that my user experience was unaffected by the change. That's what tests are for!
Dillon avatar

Dillon @d11erh

Spent the day refactoring a React component. Well written tests with react-testing-library (thanks @kentcdodds) gave me great confidence and helped me catch a subtitle error. Take-away: good unit tests are so important!

1 34

7 145

Good luck!