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

推荐订阅源

G
GRAHAM CLULEY
S
Security @ Cisco Blogs
P
Proofpoint News Feed
Cisco Talos Blog
Cisco Talos Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tor Project blog
WordPress大学
WordPress大学
Project Zero
Project Zero
S
Schneier on Security
P
Proofpoint News Feed
小众软件
小众软件
P
Privacy International News Feed
美团技术团队
L
LangChain Blog
Know Your Adversary
Know Your Adversary
J
Java Code Geeks
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
The Register - Security
The Register - Security
N
Netflix TechBlog - Medium
Microsoft Security Blog
Microsoft Security Blog
Engineering at Meta
Engineering at Meta
I
InfoQ
量子位
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
Spread Privacy
Spread Privacy
D
DataBreaches.Net
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
U
Unit 42
P
Privacy & Cybersecurity Law Blog
C
Cybersecurity and Infrastructure Security Agency CISA
T
The Blog of Author Tim Ferriss
Latest news
Latest news
K
Kaspersky official blog
MongoDB | Blog
MongoDB | Blog
L
LINUX DO - 热门话题
Simon Willison's Weblog
Simon Willison's Weblog
云风的 BLOG
云风的 BLOG
S
Securelist
AWS News Blog
AWS News Blog
F
Fortinet All Blogs
T
Threat Research - Cisco Blogs
Stack Overflow Blog
Stack Overflow Blog
Scott Helme
Scott Helme
Help Net Security
Help Net Security
Y
Y Combinator Blog
宝玉的分享
宝玉的分享
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
Tenable 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? 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
Introducing the react-testing-library 🐐
Kent C. Dodds 🏹 @kentcdodds · 2018-04-02 · via Kent C. Dodds Blog

Two weeks ago, I wrote a new library! I've been thinking about it for a while. But two weeks ago I started getting pretty serious about it:

I'm seriously starting to think that I should make my own (very small) testing lib and drop enzyme entirely. Most of enzyme's features are not at all useful (and many damaging) to my testbases. I'd rather have something smaller that encourages better practices.

17 221

Read on to get an idea of what I mean by "damaging practices."

react-testing-library

The library emoji is the goat. No particular reason...

Simple and complete React DOM testing utilities that encourage good testing practices.

The problem

You want to write maintainable tests for your React components. As a part of this goal, you want your tests to avoid including implementation details of your components and rather focus on making your tests give you the confidence for which they are intended. As part of this, you want your testbase to be maintainable in the long run so refactors of your components (changes to implementation but not functionality) don't break your tests and slow you and your team down.

This solution

The react-testing-library is a very light-weight solution for testing React components. It provides light utility functions on top of react-dom and react-dom/test-utils, in a way that encourages better testing practices. Its primary guiding principle is:

So rather than dealing with instances of rendered react components, your tests will work with actual DOM nodes. The utilities this library provides facilitate querying the DOM in the same way the user would. Finding form elements by their label text (just like a user would), finding links and buttons by their text (like a user would). It also exposes a recommended way to find elements by a data-testid as an "escape hatch" for elements where the text content and label do not make sense or is not practical.

This library encourages your applications to be more accessible and allows you to get your tests closer to using your components the way a user will, which allows your tests to give you more confidence that your application will work when a real user uses it.

This library is a replacement for enzyme. While you can follow these guidelines using enzyme itself, enforcing this is harder because of all the extra utilities that enzyme provides (utilities which facilitate testing implementation details). Read more about this in the FAQ.

Also, while the React Testing Library is intended for react-dom, you can use React Native Testing Library which has a very similar API.

What this library is not:

  1. A test runner or framework
  2. Specific to a testing framework (though we recommend Jest as our preference, the library works with any framework, and even in codesandbox!)

Examples

Basic Example

// hidden-message.js
import * as React from 'react'

// NOTE: React Testing Library works with React Hooks _and_ classes just as well
// and your tests will be the same however you write your components.
function HiddenMessage({ children }) {
	const [showMessage, setShowMessage] = React.useState(false)
	return (
		<div>
			<label htmlFor="toggle">Show Message</label>
			<input
				id="toggle"
				type="checkbox"
				onChange={(e) => setShowMessage(e.target.checked)}
				checked={showMessage}
			/>
			{showMessage ? children : null}
		</div>
	)
}

export default HiddenMessage

// __tests__/hidden-message.js
// These imports are something you'd normally configure Jest to import for you automatically.
// Learn more in the setup docs: https://testing-library.com/docs/react-testing-library/setup#skipping-auto-cleanup
import '@testing-library/jest-dom/extend-expect'
// NOTE: jest-dom adds handy assertions to Jest and is recommended, but not required

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

import HiddenMessage from '../hidden-message'

test('shows the children when the checkbox is checked', () => {
	const testMessage = 'Test Message'
	render(<HiddenMessage>{testMessage}</HiddenMessage>)

	// query* functions will return the element or null if it cannot be found
	// get* functions will return the element or throw an error if it cannot be found
	expect(screen.queryByText(testMessage)).toBeNull()

	// the queries can accept a regex to make your selectors more resilient to content tweaks and changes.
	userEvent.click(screen.getByLabelText(/show/i))

	// .toBeInTheDocument() is an assertion that comes from jest-dom
	// otherwise you could use .toBeDefined()
	expect(screen.getByText(testMessage)).toBeInTheDocument()
})

Practical Example

// login.js
import * as React from 'react'

function Login() {
	const [state, setState] = React.useReducer((s, a) => ({ ...s, ...a }), {
		resolved: false,
		loading: false,
		error: null,
	})

	function handleSubmit(event) {
		event.preventDefault()
		const { usernameInput, passwordInput } = event.target.elements

		setState({ loading: true, resolved: false, error: null })

		window
			.fetch('/api/login', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({
					username: usernameInput.value,
					password: passwordInput.value,
				}),
			})
			.then((r) => r.json())
			.then(
				(user) => {
					setState({ loading: false, resolved: true, error: null })
					window.localStorage.setItem('token', user.token)
				},
				(error) => {
					setState({ loading: false, resolved: false, error: error.message })
				},
			)
	}

	return (
		<div>
			<form onSubmit={handleSubmit}>
				<div>
					<label htmlFor="usernameInput">Username</label>
					<input id="usernameInput" />
				</div>
				<div>
					<label htmlFor="passwordInput">Password</label>
					<input id="passwordInput" type="password" />
				</div>
				<button type="submit">Submit{state.loading ? '...' : null}</button>
			</form>
			{state.error ? <div role="alert">{state.error.message}</div> : null}
			{state.resolved ? (
				<div role="alert">Congrats! You're signed in!</div>
			) : null}
		</div>
	)
}

export default Login

// __tests__/login.js
// again, these first two imports are something you'd normally handle in
// your testing framework configuration rather than importing them in every file.
import '@testing-library/jest-dom/extend-expect'

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

import Login from '../login'

test('allows the user to login successfully', async () => {
	// mock out window.fetch for the test
	const fakeUserResponse = { token: 'fake_user_token' }
	jest.spyOn(window, 'fetch').mockImplementationOnce(() => {
		return Promise.resolve({
			json: () => Promise.resolve(fakeUserResponse),
		})
	})

	render(<Login />)

	// fill out the form
	userEvent.type(screen.getByLabelText(/username/i), 'chuck')
	userEvent.type(screen.getByLabelText(/password/i), 'norris')

	userEvent.click(screen.getByText(/submit/i))

	// just like a manual tester, we'll instruct our test to wait for the alert
	// to show up before continuing with our assertions.
	const alert = await screen.findByRole('alert')

	// .toHaveTextContent() comes from jest-dom's assertions
	// otherwise you could use expect(alert.textContent).toMatch(/congrats/i)
	// but jest-dom will give you better error messages which is why it's recommended
	expect(alert).toHaveTextContent(/congrats/i)
	expect(window.localStorage.getItem('token')).toEqual(fakeUserResponse.token)
})

The most important takeaway from this example is:

The test is written in such a way that resembles how the user is using your application.

Let's explore this further...

Let's say we have a GreetingFetcher component that fetches a greeting for a user. It might render some HTML like this:

<div>
	<label for="name-input">Name</label>
	<input id="name-input" />
	<button>Load Greeting</button>
	<div data-testid="greeting-text" />
</div>

So the functionality is: Set the name, click the "Load Greeting" button, and a server request is made to load greeting text with that name.

In your test you'll need to find the <input /> so you can set its value to something. Conventional wisdom suggests you could use the id property in a CSS selector: #name-input. But is that what the user does to find that input? Definitely not! They look at the screen and find the input with the label "Name" and fill that in. So that's what our test is doing with getByLabelText. It gets the form control based on its label.

Often in tests using enzyme, to find the "Load Greeting" button you might use a CSS selector or even find by component displayName or the component constructor. But when the user wants to load the greeting, they don't care about those implementation details, instead they're going to find and click the button that says "Load Greeting." And that's exactly what our test is doing with the getByText helper!

In addition, the wait resembles exactly what the users does. They wait for the greeting text to appear, however long that takes. In our tests we're mocking that out so it happens basically instantly, but our test doesn't actually care how long it takes. We don't have to use a setTimeout in our test or anything. We simply say: "Hey, wait until the greeting-text node appears." (Note, in this case it's using a data-testid attribute which is an escape hatch for situations where it doesn't make sense to find an element by any other mechanism. A data-testid is definitely better then alternatives.

High-level Overview API

Originally, the library only provided queryByTestId as a utility as suggested in my blog post "Making your UI tests resilient to change". But thanks to feedback on that blog post from Bergé Greg as well as inspiration from a fantastic (and short!) talk by Jamie White, I added several more and now I'm even happier with this solution.

You can read more about the library and its APIs in the official docs. Here's a high-level overview of what this library gives you:

  • Simulate: a re-export from the Simulate utility from the react-dom/test-utils Simulate object.
  • wait: allows you to wait for a non-deterministic period of time in your tests. Normally you should mock out API requests or animations, but even if you're dealing with immediately resolved promises, you'll need your tests to wait for the next tick of the event loop and wait is really good for that. (Big shout out to Łukasz Gozda Gandecki who introduced this as a replacement for the (now deprecated)flushPromises API).
  • render: This is the meat of the library. It's fairly simple. It creates a divwith document.createElement, then uses ReactDOM.render to render to that div.

The render function returns the following objects and utilities:

  • container: The div your component was rendered to
  • unmount: A simple wrapper over ReactDOM.unmountComponentAtNodeto unmount your component (to facilitate easier testing of componentWillUnmount for example).
  • getByLabelText: Get a form control associated to a label
  • getByPlaceholderText: Placeholders aren't proper alternatives to labels, but if this makes more sense for your use case it's available.
  • getByText: Get any element by its text content.
  • getByAltText: Get an element (like an <img) by it's alt attribute value.
  • getByTestId: Get an element by its data-testid attribute.

Each of those get* utilities will throw a useful error message if no element can be found. There's also an associated query* API for each which will return nullinstead of throwing an error which can be useful for asserting that an element is not in the DOM.

Also, for these get* utilities, to find a matching element, you can pass:

  • a case-insensitive substring: lo world matches Hello World
  • a regex: /^Hello World$/ matches Hello World
  • a function that accepts the text and the element: (text, el) => el.tagName === 'SPAN' && text.startsWith('Hello') would match a span that has content that starts with Hello

Custom Jest Matchers

Thanks to Anto Aravinth Belgin Rayen, we have some handy custom Jest matchers as well:

  • toBeInTheDOM: Assert whether an element present in the DOM or not.
  • toHaveTextContent: Check whether the given element has a text content or not.

Note: now these have been extracted to jest-dom which is maintained by Ernesto García

Conclusion

A big feature of this library is that it doesn't have utilities that enable testing implementation details. It focuses on providing utilities that encourage good testing and software practices. I hope that by using the react-testing-libraryyour React testbases are easier to understand and maintain.