From 601c016f49719819dd74799f0b334e50311c8d88 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 5 Feb 2019 18:05:36 +0000 Subject: [PATCH] testing --- content/blog/2019-02-04-react-v16.8.0.md | 49 ++++++ content/docs/addons-test-utils.md | 194 ++++++++++++++++------- content/docs/hooks-faq.md | 62 ++++++++ 3 files changed, 246 insertions(+), 59 deletions(-) diff --git a/content/blog/2019-02-04-react-v16.8.0.md b/content/blog/2019-02-04-react-v16.8.0.md index f518ddde84c..f99297f681c 100644 --- a/content/blog/2019-02-04-react-v16.8.0.md +++ b/content/blog/2019-02-04-react-v16.8.0.md @@ -48,6 +48,55 @@ Note that React Hooks don't cover *all* use cases for classes yet but they're [v Even while Hooks were in alpha, the React community created many interesting [examples](https://codesandbox.io/react-hooks) and [recipes](https://usehooks.com) using Hooks for animations, forms, subscriptions, integrating with other libraries, and so on. We're excited about Hooks because they make code reuse easier, helping you write your components in a simpler way and make great user experiences. We can't wait to see what you'll create next! +## Testing Hooks + +We have added a new API called `ReactTestUtils.act()` in this release. It ensures that the behavior in your tests matches what happens in the browser more closely. We recommend to wrap any code rendering and triggering updates to your components into `act()` calls. Testing libraries like [`react-testing-library`](https://github.com/kentcdodds/react-testing-library) can also wrap their APIs with it. + +For example, the counter example from [this page](/docs/hooks-effect.html) can be tested like this: + +```js{3,20-22,29-31} +import React from 'react'; +import ReactDOM from 'react-dom'; +import { act } from 'react-dom/test-utils'; +import Counter from './Counter'; + +let container; + +beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); +}); + +afterEach(() => { + document.body.removeChild(container); + container = null; +}); + +it('can render and update a counter', () => { + // Test first render and componentDidMount + act(() => { + ReactDOM.render(, container); + }); + const button = container.querySelector('button'); + const label = container.querySelector('p'); + expect(label.textContent).toBe('You clicked 0 times'); + expect(document.title).toBe('You clicked 0 times'); + + // Test second render and componentDidUpdate + act(() => { + button.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(label.textContent).toBe('You clicked 1 times'); + expect(document.title).toBe('You clicked 1 times'); +}); +``` + +The calls to `act()` will also flush the effects inside of them. + +If you need to test a custom Hook, you can do so by creating a component in your test, and using your Hook from it. Then you can test the component you wrote. + +To reduce the boilerplate, we recommend using [`react-testing-library`](https://git.io/react-testing-library) which is designed to encourage writing tests that use your components as the end users do. + ## Thanks We'd like to thank everybody who commented on the [Hooks RFC](https://github.com/reactjs/rfcs/pull/68) for sharing their feedback. We've read all of your comments and made some adjustments to the final API based on them. diff --git a/content/docs/addons-test-utils.md b/content/docs/addons-test-utils.md index c7ef5c023ef..40cec512c3b 100644 --- a/content/docs/addons-test-utils.md +++ b/content/docs/addons-test-utils.md @@ -19,12 +19,11 @@ var ReactTestUtils = require('react-dom/test-utils'); // ES5 with npm > Note: > -> Airbnb has released a testing utility called Enzyme, which makes it easy to assert, manipulate, and traverse your React Components' output. If you're deciding on a unit testing utility to use together with Jest, or any other test runner, it's worth checking out: [http://airbnb.io/enzyme/](http://airbnb.io/enzyme/) +> We recommend using [`react-testing-library`](https://git.io/react-testing-library) which is designed to enable and encourage writing tests that use your components as the end users do. > -> Alternatively, there is another testing utility called react-testing-library designed to enable and encourage writing tests that use your components as the end users use them. It also works with any test runner: [https://git.io/react-testing-library](https://git.io/react-testing-library) +> Alternatively, Airbnb has released a testing utility called [Enzyme](http://airbnb.io/enzyme/), which makes it easy to assert, manipulate, and traverse your React Components' output. - - [`Simulate`](#simulate) - - [`renderIntoDocument()`](#renderintodocument) + - [`act()`](#act) - [`mockComponent()`](#mockcomponent) - [`isElement()`](#iselement) - [`isElementOfType()`](#iselementoftype) @@ -38,68 +37,88 @@ var ReactTestUtils = require('react-dom/test-utils'); // ES5 with npm - [`findRenderedDOMComponentWithTag()`](#findrendereddomcomponentwithtag) - [`scryRenderedComponentsWithType()`](#scryrenderedcomponentswithtype) - [`findRenderedComponentWithType()`](#findrenderedcomponentwithtype) + - [`renderIntoDocument()`](#renderintodocument) + - [`Simulate`](#simulate) ## Reference -## Shallow Rendering - -When writing unit tests for React, shallow rendering can be helpful. Shallow rendering lets you render a component "one level deep" and assert facts about what its render method returns, without worrying about the behavior of child components, which are not instantiated or rendered. This does not require a DOM. - -> Note: -> -> The shallow renderer has moved to `react-test-renderer/shallow`.
-> [Learn more about shallow rendering on its reference page.](/docs/shallow-renderer.html) - -## Other Utilities - -### `Simulate` - -```javascript -Simulate.{eventName}( - element, - [eventData] -) +### `act()` + +To prepare a component for assertions, wrap the code rendering it and performing updates inside an `act()` call. This makes your test run closer to how React works in the browser. + +For example, let's say we have this `Counter` component: + +```js +class App extends React.Component { + constructor(props) { + super(props); + this.state = {count: 0}; + this.handleClick = this.handleClick.bind(this); + } + componentDidMount() { + document.title = `You clicked ${this.state.count} times`; + } + componentDidUpdate() { + document.title = `You clicked ${this.state.count} times`; + } + handleClick() { + this.setState(state => ({ + count: state.count + 1, + })); + } + render() { + return ( +
+

You clicked {this.state.count} times

+ +
+ ); + } +} ``` -Simulate an event dispatch on a DOM node with optional `eventData` event data. - -`Simulate` has a method for [every event that React understands](/docs/events.html#supported-events). - -**Clicking an element** - -```javascript -// -const node = this.button; -ReactTestUtils.Simulate.click(node); +Here is how we can test it: + +```js{3,20-22,29-31} +import React from 'react'; +import ReactDOM from 'react-dom'; +import { act } from 'react-dom/test-utils'; +import Counter from './Counter'; + +let container; + +beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); +}); + +afterEach(() => { + document.body.removeChild(container); + container = null; +}); + +it('can render and update a counter', () => { + // Test first render and componentDidMount + act(() => { + ReactDOM.render(, container); + }); + const button = container.querySelector('button'); + const label = container.querySelector('p'); + expect(label.textContent).toBe('You clicked 0 times'); + expect(document.title).toBe('You clicked 0 times'); + + // Test second render and componentDidUpdate + act(() => { + button.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(label.textContent).toBe('You clicked 1 times'); + expect(document.title).toBe('You clicked 1 times'); +}); ``` -**Changing the value of an input field and then pressing ENTER.** - -```javascript -// this.textInput = node} /> -const node = this.textInput; -node.value = 'giraffe'; -ReactTestUtils.Simulate.change(node); -ReactTestUtils.Simulate.keyDown(node, {key: "Enter", keyCode: 13, which: 13}); -``` - -> Note -> -> You will have to provide any event property that you're using in your component (e.g. keyCode, which, etc...) as React is not creating any of these for you. - -* * * - -### `renderIntoDocument()` - -```javascript -renderIntoDocument(element) -``` - -Render a React element into a detached DOM node in the document. **This function requires a DOM.** - -> Note: -> -> You will need to have `window`, `window.document` and `window.document.createElement` globally available **before** you import `React`. Otherwise React will think it can't access the DOM and methods like `setState` won't work. +Don't forget that dispatching DOM events only works when the DOM container is added to the `document`. You can use a helper like [`react-testing-library`](https://github.com/kentcdodds/react-testing-library) to reduce the boilerplate code. * * * @@ -265,5 +284,62 @@ findRenderedComponentWithType( Same as [`scryRenderedComponentsWithType()`](#scryrenderedcomponentswithtype) but expects there to be one result and returns that one result, or throws exception if there is any other number of matches besides one. +*** + +### `renderIntoDocument()` + +```javascript +renderIntoDocument(element) +``` + +Render a React element into a detached DOM node in the document. **This function requires a DOM.** It is effectively equivalent to: + +```js +const domContainer = document.createElement('div'); +ReactDOM.render(element, domContainer); +``` + +> Note: +> +> You will need to have `window`, `window.document` and `window.document.createElement` globally available **before** you import `React`. Otherwise React will think it can't access the DOM and methods like `setState` won't work. + * * * +## Other Utilities + +### `Simulate` + +```javascript +Simulate.{eventName}( + element, + [eventData] +) +``` + +Simulate an event dispatch on a DOM node with optional `eventData` event data. + +`Simulate` has a method for [every event that React understands](/docs/events.html#supported-events). + +**Clicking an element** + +```javascript +// +const node = this.button; +ReactTestUtils.Simulate.click(node); +``` + +**Changing the value of an input field and then pressing ENTER.** + +```javascript +// this.textInput = node} /> +const node = this.textInput; +node.value = 'giraffe'; +ReactTestUtils.Simulate.change(node); +ReactTestUtils.Simulate.keyDown(node, {key: "Enter", keyCode: 13, which: 13}); +``` + +> Note +> +> You will have to provide any event property that you're using in your component (e.g. keyCode, which, etc...) as React is not creating any of these for you. + +* * * \ No newline at end of file diff --git a/content/docs/hooks-faq.md b/content/docs/hooks-faq.md index 91c695299e6..068180bd1d3 100644 --- a/content/docs/hooks-faq.md +++ b/content/docs/hooks-faq.md @@ -113,8 +113,70 @@ Importantly, custom Hooks give you the power to constrain React API if you'd lik From React's point of view, a component using Hooks is just a regular component. If your testing solution doesn't rely on React internals, testing components with Hooks shouldn't be different from how you normally test components. +For example, let's say we have this counter component: + +```js +function Example() { + const [count, setCount] = useState(0); + useEffect(() => { + document.title = `You clicked ${count} times`; + }); + return ( +
+

You clicked {count} times

+ +
+ ); +} +``` + +We'll test it using React DOM. To make sure that the behavior matches what happens in the browser, we'll wrap the code rendering and updating it into [`ReactTestUtils.act()`](/docs/test-utils.html#act) calls: + +```js{3,20-22,29-31} +import React from 'react'; +import ReactDOM from 'react-dom'; +import { act } from 'react-dom/test-utils'; +import Counter from './Counter'; + +let container; + +beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); +}); + +afterEach(() => { + document.body.removeChild(container); + container = null; +}); + +it('can render and update a counter', () => { + // Test first render and componentDidMount + act(() => { + ReactDOM.render(, container); + }); + const button = container.querySelector('button'); + const label = container.querySelector('p'); + expect(label.textContent).toBe('You clicked 0 times'); + expect(document.title).toBe('You clicked 0 times'); + + // Test second render and componentDidUpdate + act(() => { + button.dispatchEvent(new MouseEvent('click', {bubbles: true})); + }); + expect(label.textContent).toBe('You clicked 1 times'); + expect(document.title).toBe('You clicked 1 times'); +}); +``` + +The calls to `act()` will also flush the effects inside of them. + If you need to test a custom Hook, you can do so by creating a component in your test, and using your Hook from it. Then you can test the component you wrote. +To reduce the boilerplate, we recommend using [`react-testing-library`](https://git.io/react-testing-library) which is designed to encourage writing tests that use your components as the end users do. + ### What exactly do the [lint rules](https://www.npmjs.com/package/eslint-plugin-react-hooks) enforce? We provide an [ESLint plugin](https://www.npmjs.com/package/eslint-plugin-react-hooks) that enforces [rules of Hooks](/docs/hooks-rules.html) to avoid bugs. It assumes that any function starting with "`use`" and a capital letter right after it is a Hook. We recognize this heuristic isn't perfect and there may be some false positives, but without an ecosystem-wide convention there is just no way to make Hooks work well -- and longer names will discourage people from either adopting Hooks or following the convention.