Commit cae35300 authored by Kristian Freeman's avatar Kristian Freeman

Add unit-testing-with-react-testing-library

parent 3d40ab56
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*
This diff is collapsed.
{
"name": "unit-testing-with-react-testing-library",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@sheerun/mutationobserver-shim": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz",
"integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==",
"dev": true
},
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
"dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"dom-testing-library": {
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/dom-testing-library/-/dom-testing-library-3.12.2.tgz",
"integrity": "sha512-kMj5UFm5lSxnMDtD6XWkJuzK0CX+XdMd5NSnvtpneeYQFORTF5Q5C6mdweH6hhwFcKYzAsHCfnvldg9c8HuFOA==",
"dev": true,
"requires": {
"@sheerun/mutationobserver-shim": "^0.3.2",
"pretty-format": "^23.6.0",
"wait-for-expect": "^1.0.0"
}
},
"pretty-format": {
"version": "23.6.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.6.0.tgz",
"integrity": "sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==",
"dev": true,
"requires": {
"ansi-regex": "^3.0.0",
"ansi-styles": "^3.2.0"
}
},
"react-testing-library": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/react-testing-library/-/react-testing-library-5.2.3.tgz",
"integrity": "sha512-Bw52++7uORuIQnL55lK/WQfppqAc9+8yFG4lWUp/kmSOvYDnt8J9oI5fNCfAGSQi9iIhAv9aNsI2G5rtid0nrA==",
"dev": true,
"requires": {
"dom-testing-library": "^3.12.0"
}
},
"wait-for-expect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.0.1.tgz",
"integrity": "sha512-TPZMSxGWUl2DWmqdspLDEy97/S1Mqq0pzbh2A7jTq0WbJurUb5GKli+bai6ayeYdeWTF0rQNWZmUvCVZ9gkrfA==",
"dev": true
}
}
}
{
"name": "unit-testing-with-react-testing-library",
"version": "0.1.0",
"private": true,
"dependencies": {
"enzyme": "^2.9.1",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-test-renderer": "^15.6.1"
},
"devDependencies": {
"jest-dom": "^2.1.1",
"react-scripts": "0.9.5",
"react-testing-library": "^5.2.3"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tag above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start`.
To create a production bundle, use `npm run build`.
-->
</body>
</html>
import React from "react";
import ReactDOM from "react-dom";
class MyComponent extends React.Component {
constructor() {
super();
this.state = {
num: 0,
set: [1, 2, 3]
};
}
incrementNum() {
this.setState({ num: this.state.num + 1 });
}
// No changes are needed below this line.
render() {
const { num, set } = this.state;
const { myThing } = this.props;
return (
<div>
<button onClick={() => this.incrementNum()}>Click me!</button>
<span data-testid="num">The num is {num}</span>
<ul data-testid="list">
{set.map(s => (
<li key={s}>{s}</li>
))}
</ul>
{myThing && <span>My thing is {myThing}</span>}
</div>
);
}
}
const root = document.getElementById("root");
if (root) {
ReactDOM.render(<MyComponent />, root);
}
export default MyComponent;
import React from "react";
import ReactDOM from "react-dom";
import { shallow } from "enzyme";
import { fireEvent, render } from "react-testing-library";
import MyComponent from "./index";
// In this exercise, we'll progressively move each
// enzyme-based test from this previous exercise over to using
// react-testing-library. react-testing-library is designed
// with a number of improvements in design/functionality over enzyme,
// with the primary decision being to remove shallow rendering
// entirely, and focus on true testing of components and behavior,
// using *real* DOM elements and *real* event handling.
//
// Let's begin with a simple example.
test("it renders myThing prop", () => {
const myThing = "foobar";
// In our current enzyme example, we're using shallow rendering
// to render a single layer of our component tree - in this case,
// just MyComponent. Because our component just renders a single
// layer, we're only using shallow for the API, not necessarily
// for the "shallow" functionality itself.
//
// Let's switch over to using react-testing-library. The lib
// exposes a few functions, the primary of being `render`.
// Call the render function, passing in the rendered component
// in the same way that it has been called for shallow (you can
// even just copy paste the shallow function call, and replace
// shallow with render.
//
// Now for the interesting part. react-testing-library returns an
// object from render - the object is intended to be destructured,
// so you can get access to the methods that you want to use for
// testing and assertions.
//
// const { getByText } = render(<TestComponent />)
//
// In this test, we want to test for a text value in the component,
// based on the myThing prop. We know that in MyComponent, the
// myThing prop is used inside of a span that displays the text
// 'My thing is {myThing}'. Use the render function and destructure
// getByText out from the returned object (like in the above example).
// Call the getByText function, passing in the interpolated string
// 'My thing is {myThing}' (look up the ES6 string interpolation
// syntax if you need help!). Set the value of this function call
// to textNode.
//
// Now we have a textNode we can make an assertion with. We
// just want to test that the textNode *exists*, we can simply
// assert using `toBeTruthy`:
//
// expect(1).toBeTruthy()
//
// Make this assertion with textNode.
const component = shallow(<MyComponent myThing={myThing} />);
const node = <span>My thing is {myThing}</span>;
expect(component.contains(node)).toEqual(true);
});
test("can find the num", () => {
// getByText is just one of many available methods returned
// from react-testing-library. Another useful tool is `getByTextId`.
//
// One of the most unreliable methods in enzyme is `find`. Because
// it requires looking up by a specific DOM tag, class, or ID, it's
// prone to failure if you change an expected value: for instance,
// if the test is looking for a span tag, but you've changed it to
// an h4, the test will fail. This is a "brittle" test: one that
// is slightly unreliable.
//
// The author of react-testing-library has a pretty interesting
// solution to this: using the "data-" attribute behavior in HTML
// to set a "test id" on specific DOM elements, which we can use
// to reliably look up specific DOM elements. You can read more
// here (and I recommend reading it! It's one of the best JS blog
// posts I've read in quite some time):
//
// https://blog.kentcdodds.com/making-your-ui-tests-resilient-to-change-d37a6ee37269
//
// Let's try using the getByTestId function in react-testing-library
// to update this "num" test. Right now we're using component.text()
// to get the full text of the component, and using .includes to
// find a substring inside of that text. Call the render function,
// passing in MyComponent, and destructure the function getByTestId
// out of the value returned.
//
// Let's modify this test slightly and set a numNode variable to the
// value of calling getByTestId, passing in the argument "num" as a
// test ID for looking up the correct field. Where does this come from?
// I've gone and updated src/index.js with a few data-test-id attributes
// in the render function! Go take a look at those to see the full workflow
// between your component's render function, and getByTestId inside your tests.
//
// Once you've done this, make a similar assertion to the previous test:
// we expect numNode to *exist*, or have a *truthy* value - how do we assert
// that in Jest + react-testing-library?
const component = shallow(<MyComponent />);
const renderedComponentText = component.text();
const assertion = renderedComponentText.includes("The num is 0");
expect(assertion).toEqual(true);
});
test("can find the list", () => {
// Let's replace this test with another usage of
// getByTestId. We want to find the ul element,
// which has a data-test-id attribute of "test".
//
// Using the previous test as a reference, set
// listNode to the value of calling getByTestId
// with our list's test id.
//
// Once we've found listNode, we should assert
// that it has three children, similar to our
// current version of the test. We can do this
// by using the `toHaveLength` check:
//
// expect(array).toHaveLength(5)
//
// If we wanted to check the length of the
// listNode's children (available at
// listNode.children), how would we format that
// assertion?
const component = shallow(<MyComponent />);
const list = component.find("ul");
const assertion = list.children().length;
expect(assertion).toEqual(3);
});
test("clicking the button increments num", () => {
// Finally, we'll use react-testing-library
// to handle interactivity. One issue with enzyme
// is the idea of "simulating" events: as the author
// of react-testing-library says:
//
// "The more your tests resemble the way your software
// is used, the more confidence they can give you."
// - https://twitter.com/kentcdodds/status/977018512689455106
//
// *Simulating* a click is *not* how your software is
// used! The user *actually* clicks the button.
// react-testing-library has tools for interacting with
// buttons, inputs, and any other HTML that the user would
// normally interact with, but importantly, it *actually*
// performs/fires events on those HTML elements, and doesn't
// just simulate them.
//
// Review this test as it is before we move on to rewriting it.
// In this test, we check a value in state, simulate a click, and
// then check the new value in state. The success case for this
// test is that `num` inside of state starts at 0, and then
// increments to 1.
//
// This state value is represented in our UI via a span that
// renders the text "The num is {num}". If we want to _actually_
// test the functionality of our component, and not just the
// implementation, we should validate that text, instead of the
// state object inside of the component.
//
// First, let's check the text at initial render. Call the
// render function, passing in MyComponent, and destructure
// getByText out of the object returned from render.
// Set numNode to the value of calling getByText, looking for
// the text "The num is {num}". Replace "{num}" with the
// *initial value of num in your state*. Assert that this
// numNode is truthy.
//
// Now we want to actually fire an event. react-testing-library
// allows you to import a named function, fireEvent, from inside
// of the package. We want to find the button in our component, and
// pass it into the function fireEvent.click:
//
// fireEvent.click(getByText('My button text'))
//
// Look at the definition of MyComponent, and rewrite (and call)
// the above example code, replacing 'My button text' with the actual
// text for our button.
//
// Now that we've *actually* clicked the button (and not just simulated
// it), we should check the new value of numNode, since the text should
// have changed with our new num value. Set numNode to the value of
// "The num is {num}", once again replacing "{num}" with the **new** value
// of your num in state. Make another assertion that this new version
// of numNode is truthy.
//
// Notice that we're testing the UI here, not the implementation! If
// we want to test the *actual* behavior of our application, and what's
// useful to our users, this means testing the text that is rendered based
// on our num in state, not just the state and num value themselves!
const component = shallow(<MyComponent />);
expect(component.state().num).toEqual(0);
const button = component.find("button");
button.simulate("click");
expect(component.state().num).toEqual(1);
});
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>
import 'jest-dom/extend-expect'
import 'react-testing-library/cleanup-after-each'
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment