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

推荐订阅源

K
Kaspersky official blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
V
Visual Studio Blog
F
Full Disclosure
B
Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
L
Lohrmann on Cybersecurity
月光博客
月光博客
I
Intezer
博客园 - 三生石上(FineUI控件)
Hacker News - Newest:
Hacker News - Newest: "LLM"
D
Darknet – Hacking Tools, Hacker News & Cyber Security
博客园_首页
P
Proofpoint News Feed
C
Check Point Blog
N
News | PayPal Newsroom
H
Heimdal Security Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
G
GRAHAM CLULEY
WordPress大学
WordPress大学
C
CERT Recently Published Vulnerability Notes
Y
Y Combinator Blog
Recorded Future
Recorded Future
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog
W
WeLiveSecurity
L
LINUX DO - 热门话题
Microsoft Azure Blog
Microsoft Azure Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Schneier on Security
Schneier on Security
爱范儿
爱范儿
Martin Fowler
Martin Fowler
U
Unit 42
T
Troy Hunt's Blog
S
Securelist
V
V2EX
V2EX - 技术
V2EX - 技术
MongoDB | Blog
MongoDB | Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
博客园 - 聂微东
人人都是产品经理
人人都是产品经理
M
MIT News - Artificial intelligence
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
罗磊的独立博客
小众软件
小众软件
阮一峰的网络日志
阮一峰的网络日志
Vercel News
Vercel News

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? Stop mocking fetch 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 Profile a React App for Performance
But really, what is a JavaScript mock?
2018-03-19 · via Kent C. Dodds Blog

This is a great follow-up to But really, what is a JavaScript test? So here we go!


Step 0

To learn about mocks we have to have something to test and something to mock, so here's the module we'll be testing today:

// thumb-war.js
import { getWinner } from './utils'

function thumbWar(player1, player2) {
	const numberToWin = 2
	let player1Wins = 0
	let player2Wins = 0
	while (player1Wins < numberToWin && player2Wins < numberToWin) {
		const winner = getWinner(player1, player2)
		if (winner === player1) {
			player1Wins++
		} else if (winner === player2) {
			player2Wins++
		}
	}
	return player1Wins > player2Wins ? player1 : player2
}

export default thumbWar

It's a thumb war game where you play best 2 out of three. It uses a function called getWinner from utils. getWinner returns the winning player or null for a tie. We're going to pretend this is making a call to some third party machine learning service that has a testing environment we don't control and is unreliable so we want to mock it out for tests. This is one of the (rare) situations where mocking is really your only choice to reliably test your code. (I'm still making it synchronous to simplify our example further).

In addition, unless we re-implement all the inner-workings of getWinner in our tests, there's no way for us to really make useful assertions because the winner of the thumb war is non-deterministic. So without mocking anything, here's the best our test can do:

// thumb-war.0.js
import thumbWar from '../thumb-war'

test('returns winner', () => {
	const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
	expect(['Ken Wheeler', 'Kent C. Dodds'].includes(winner)).toBe(true)
})

We can only assert that the winner is one of the players, and maybe that's enough. But if we really want to ensure that our thumbWarfunction is integrating properly with getWinner (as much as we reasonably can), then we'll want to create a mock for it and assert on a real winner.

Step 1

The simplest form of mocking is monkey-patching values. Here's an example of what our test looks like when we do that:

import thumbWar from '../thumb-war'
import * as utils from '#app/utils'

test('returns winner', () => {
	const originalGetWinner = utils.getWinner
	// eslint-disable-next-line import/namespace
	utils.getWinner = (p1, p2) => p2

	const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
	expect(winner).toBe('Kent C. Dodds')

	// eslint-disable-next-line import/namespace
	utils.getWinner = originalGetWinner
})

You'll notice a few things here. First we have to import the utils module as a * import so we have an object that we can manipulate (NOTE: read that with a grain of salt! More on why this is bad later). Then we need to store the original function at the beginning of our test and restore it at the end so other tests aren't impacted by the changes we're making to the utils module.

All of that is just setup for the actual mocking part of our changes. The mock is the line that reads:

utils.getWinner = (p1, p2) => p2

This is monkey-patching mocking. It's effective (we're now able to ensure there's a specific winner of the thumbWar game), but there are some limitations to this. One thing that's annoying is the eslint warning, so we've disabled that (again, don't actually do this as it makes your code non-spec compliant! Again, more on this later). Also, we don't actually know for sure whether the utils.getWinner function was called as much as it should have been (twice, for a best 2 out of 3 game). This may or may not be important for the application, but it's important for what I'm trying to teach you so let's improve that!

Step 2

Let's add some code to make sure that the getWinner function was called twice, and ensure it was called with the right arguments.

import thumbWar from '../thumb-war'
import * as utils from '#app/utils'

test('returns winner', () => {
	const originalGetWinner = utils.getWinner
	// eslint-disable-next-line import/namespace
	utils.getWinner = (...args) => {
		utils.getWinner.mock.calls.push(args)
		return args[1]
	}
	utils.getWinner.mock = { calls: [] }

	const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
	expect(winner).toBe('Kent C. Dodds')
	expect(utils.getWinner.mock.calls).toHaveLength(2)
	utils.getWinner.mock.calls.forEach((args) => {
		expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
	})

	// eslint-disable-next-line import/namespace
	utils.getWinner = originalGetWinner
})

So here we're adding a mock object to our mock function so we can keep some mock metadata about how the function is called. This allows us to add these two assertions:

expect(utils.getWinner.mock.calls).toHaveLength(2)
utils.getWinner.mock.calls.forEach((args) => {
	expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
})

This helps us ensure that our mock is being called properly (with the right arguments) and that it's being called the right number of times (twice for a two out of three game).

Now so long as our mock can model what the real world version does, we can get back a little confidence that our code is working despite having to mock out what getWinner is actually doing. It may not be a bad idea to implement some contract testing to ensure that the contract between getWinner and the third party service is kept in check. But I'll leave that to your imagination!

Step 3

So all of this stuff is cool, but it's annoying to have to keep track of when our mock is called all the time. Turns out that what we've done is manually implement a mock function and Jest comes built-in with a utility for exactly this. So let's simplify our code by using that!

import thumbWar from '../thumb-war'
import * as utils from '#app/utils'

test('returns winner', () => {
	const originalGetWinner = utils.getWinner
	// eslint-disable-next-line import/namespace
	utils.getWinner = jest.fn((p1, p2) => p2)

	const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
	expect(winner).toBe('Kent C. Dodds')
	expect(utils.getWinner).toHaveBeenCalledTimes(2)
	utils.getWinner.mock.calls.forEach((args) => {
		expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
	})

	// eslint-disable-next-line import/namespace
	utils.getWinner = originalGetWinner
})

Here we've simply wrapped our getWinner mock implementation with jest.fn. This effectively does all the same stuff we were doing, except because it's a special Jest mock function, there are some special assertions we can use just for that purpose (like toHaveBeenCalledTimes). Jest has an assertion called toHaveBeenNthCalledWith, so we could have avoided our forEach, but I think it's ok as it is (and luckily we implemented our own metadata collection in the same way Jest does, so we don't need to change that assertion. Fancy that!).

The next thing I don't like is having to keep track of originalGetWinner and restore that at the end. I'm also bothered by those eslint comments I had to put there (remember! That rule is super important and we'll talk about it in a moment). Let's see if we can simplify things further with another Jest utility.

Step 4

Luckily, Jest has a utility called spyOn which does exactly what we need:

import thumbWar from '../thumb-war'
import * as utils from '#app/utils'

test('returns winner', () => {
	jest.spyOn(utils, 'getWinner')
	utils.getWinner.mockImplementation((p1, p2) => p2)

	const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
	expect(winner).toBe('Kent C. Dodds')

	utils.getWinner.mockRestore()
})

Sweet! We've really simplified things! Mock functions are also called spies (which is why the API for this is called spyOn). By default, Jest will just keep the original implementation of getWinner but still keep track of how it's called. For us though we don't want the original implementation to be called so we use mockImplementation to mock out what happens when it's called. Then at the end we use mockRestore to clean up after ourselves just as we were before. Neat right!?

So remember the eslint errors we were seeing? Let's address those next!

Step 5

The ESLint error we were seeing is actually really important. We got around the issue because we change our code in such a way that eslint-plugin-import was unable to statically detect that we are still actually breaking the rule. But this rule is actually very important. The rule is: import/namespace. The reason it's broken in this case is:

Reports on assignment to a member of an imported namespace.

So why is this a problem? It's because the fact that our code works is just the luck of how Babel transpiles it to CommonJS and how the require cache works. When I import a module, I'm importing immutable bindings to the functions in that module, so if I import the same module in two different files and attempt to mutate the bindings, the mutation will only apply for the module where the mutation occurred (I'm actually not sure about this, I may get an error, which would probably be better). So if you rely on this, you're probably in for tears when you try to upgrade to ES modules for realzies.

That said, what we're about to do doesn't really comply with the spec either (it's test utilities doing some magic for us), but our code looks like it complies with the spec which is important so folks on the team don't learn bad habits that could find their way into application code.

So to solve this, we could attempt to muck with the require.cacheto swap the actual implementation of the module for our mock version, but we'd find out that imports happen before our code runs and so we wouldn't be able to run it in time without pulling it into another file. Also, my kids are about to wake up and I gotta get this done!

So now we come to the jest.mock API. Because Jest actually simulates the module system for us, it can very easily and seamlessly swap out a mock implementation of a module for the real one! Here's what our test looks like now:

import thumbWar from '../thumb-war'
import * as utilsMock from '#app/utils'

jest.mock('#app/utils', () => {
	return {
		getWinner: jest.fn((p1, p2) => p2),
	}
})

test('returns winner', () => {
	const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
	expect(winner).toBe('Kent C. Dodds')
	expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
	utilsMock.getWinner.mock.calls.forEach((args) => {
		expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
	})
})

Cool right!? We just tell Jest we want all files to use our mock version instead and poof! It does! Notice also that I changed the name of the import from utils to utilsMock. That's not required, but I like doing that to communicate the intention that this should be importing a mocked version of the module, not the real thing.

Common question: If you only want to mock one of several functions in a module, then you may like the jest.requireActual API.

Step 6

Ok, so we're almost done. What if we're using this getWinner function in several of our tests and we don't want to copy/paste this mock everywhere? That's where the __mocks__ directory comes in handy! So we create a __mocks__ directory right next to the file that we want to mock, and then create a file with the same name:

other/whats-a-mock/
├── __mocks__
│   └── utils.js
├── __tests__/
├── thumb-war.js
└── utils.js

Inside the __mocks__/utils.js file, we'll put this:

// __mocks__/utils.js
export const getWinner = jest.fn((p1, p2) => p2)

And with that, we can update our test:

// __tests__/thumb-war.js
import thumbWar from '../thumb-war'
import * as utilsMock from '#app/utils'

jest.mock('#app/utils')

test('returns winner', () => {
	const winner = thumbWar('Ken Wheeler', 'Kent C. Dodds')
	expect(winner).toBe('Kent C. Dodds')
	expect(utilsMock.getWinner).toHaveBeenCalledTimes(2)
	utilsMock.getWinner.mock.calls.forEach((args) => {
		expect(args).toEqual(['Ken Wheeler', 'Kent C. Dodds'])
	})
})

🎉 Woo! Now we just say jest.mock(pathToModule) and it'll pick up the mock file we created for us automatically.

Now we may not want this mock to always return the second player, so we can use mockImplementation for specific tests to verify that it works if we return the second and then first and then second again, etc. Feel free to try that on your own. You can also equip your mock with some utilities as well if you like. The world is your oyster.

Good luck!