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

推荐订阅源

cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
月光博客
月光博客
T
Tenable Blog
博客园_首页
P
Privacy International News Feed
博客园 - 聂微东
S
Schneier on Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
MongoDB | Blog
MongoDB | Blog
T
Tor Project blog
The Hacker News
The Hacker News
P
Proofpoint News Feed
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 司徒正美
C
Check Point Blog
V
Vulnerabilities – Threatpost
博客园 - 三生石上(FineUI控件)
N
Netflix TechBlog - Medium
Recent Announcements
Recent Announcements
T
Troy Hunt's Blog
L
LangChain Blog
Project Zero
Project Zero
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Cloudflare Blog
Latest news
Latest news
Security Archives - TechRepublic
Security Archives - TechRepublic
Help Net Security
Help Net Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
L
Lohrmann on Cybersecurity
V
Visual Studio Blog
D
Docker
I
Intezer
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The GitHub Blog
The GitHub Blog
博客园 - 叶小钗
L
LINUX DO - 热门话题
The Last Watchdog
The Last Watchdog
NISL@THU
NISL@THU
大猫的无限游戏
大猫的无限游戏
T
Tailwind CSS Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
www.infosecurity-magazine.com
www.infosecurity-magazine.com
SecWiki News
SecWiki News
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件

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!