What makes a good test? Outline of this article Pt 1: Why do we write tests?* To prove our code works* To protect our code from breaking when we work on it* To document behavior* To help make design decisions Pt 2: Testing practices* TDD* Watch mode Pt 3: Testing React* Recommended libraries* Snapshots vs assertions* Rules of thumb for assertions* Rules of thumb for snapshots* Black Box testing* What is the public API for a React component?* What is not part of the public API for a React component? Pt 4: More testing practices* Testing async code* Testing library code* Quotes about testing Why do we write tests? To prove our code works To protect our code from breaking when we work on it To document behavior To help make design decisions To prove our code works Tests should capture the of a feature by describing . intent desired behavior If you are working from a spec, user story, or design doc, the acceptance criteria are a good starting point for a test suite. Tests should cover not only the “happy path” but also ensure that empty states, missing data, and bad data are handled safely. If a function can throw an error, tests should validate the error. Example: describing what the app should do describe('MyList', () => { test( 'should sort alphabetically when the "name" header is clicked' ) }) describe('An admin', () => { describe('with no teams', () => { test('should see the empty teams page') test('should see the New Team button') }) describe('with existing teams', () => { test('should a list of teams') }) }) To protect our code from breaking when we work on it A comprehensive test suite allows us to move quickly: I’ve seen the continuous delivery process running smoothly at dozens of organizations — but I’ve never seen it work anywhere without a quality array of test suites that includes both unit tests and functional tests, and frequently includes integration tests, as well. — Eric Elliot Example: describing expected states import Header from "./Header"; it("should render signed out by default", () => { expect.assertions(2) const tree = mount(<Header />); const markers = { in: tree.find({"data-test": "signedin"}), out: tree.find({"data-test": "signedout"}) }; expect(markers.in).toHaveLength(0); expect(markers.out).toHaveLength(1); }); To document behavior Sometimes tests are the only place a function is fully documented. This is particular true of edge cases that might not show up in normal usage. Make sure to cover those cases! Snapshots in particular can help quickly document a range of possible scenarios (at the loss of specificity). Example describe('Component states', () => { test("loading state snapshot should match", () => { const snap = shallow(<Foo loading />); expect(snap).toMatchSnapshot(); }); test("empty state snapshot should match", () => { const snap = shallow(<Foo items={[]} />); expect(snap).toMatchSnapshot(); }); it("happy path state snapshot should match", () => { const snap = shallow(<Foo items={dummyItemsList} />); expect(snap).toMatchSnapshot(); }); it("error state snapshot should match", () => { const snap = shallow(<Foo items={dummyInvalidItemsList} />); expect(snap).toMatchSnapshot(); }); }) To help make design decisions Writing tests early in the development process helps to clarify the relationships between components and identify problems like coupling, hard-to-use APIs, and separation-of-concerns violations. Test-Driven Development Test-driven development ( ) advocates recommend : Write a test, then write ; write another test, make it pass, and so on, one assertion at a time. Even if you aren’t practicing TDD it’s a good idea to be aware of the practice and what it is trying to achieve. TDD writing tests before code just the code to make it pass Watch Mode It’s a good idea to use your test runner’s “watch mode” to continuously run tests while you work. In Jest you can pass a flag: jest --watch. Many editors also have plugins to display test output within the IDE. Testing React Recommended Libraries — test runner, assertions, mocks Jest Update 2018–07–02: I now recommend over for testing components react-testing-library Enzyme — stubs and mocks (Jest has built-ins for some of this functionality so you might not need it) Sinon yarn add --dev jest react-testing-library sinon Render components: // importing renderers for snapshotting and DOM assertions import { render } from "react-testing-library"; Snapshots vs Assertions Jest comes with a toMatchSnapshot expectation lets you compare a data structure to a stored copy that is checked into source control. Snapshots have advantages: They are easy to create, easy to update, and can act as a red flag if updating one component causes snapshots in other areas to fail. On the other hand, it's very easy to accidentally update a snapshot without verifying that the change makes sense. It's also hard to tell exactly what details are important in a snapshot and which aren't, whereas focused assertions can allow implementation changes while still preventing regressions. Rule of thumb for assertions: Interact with the DOM the way users do — using semantic HTML targets, label text, and ARIA attributes. If none of those make sense, use test markers (such as data-test="xyz") instead of targeting id or CSS classes. Don’t couple the test to the implementation — the test shouldn’t fail when something happens that a user wouldn’t notice, like adding a wrapper element or tweaking a class name. Note: If you don’t want to ship unused data-attributes to production, there are Babel plugins to remove properties from React components. Rules of thumb for snapshots: Keep snapshots small enough that they can be verified manually. A huge snapshot diff is more likely to get updated without a close look. Open the snapshot files to make sure you aren’t snapshotting a loading state or empty element unintentionally. Run in watch mode while working with snapshots so that you are making small changes instead of big, monolithic updates. Don’t forget to hydrate components with mock data — -a snapshot of a component without most of its props is not a realistic representation of how it is used in the app. “Black Box” Testing Tests should use public APIs. This means that tests use the same interfaces as “real” (application) code. A component under test should be treated as a that takes inputs and returns outputs; the test should verify that, given a certain input or combination of inputs, the correct output comes out. “black box” What is the public API for a React Component? Inputs Props (rarely) Context Outputs Rendered UI/DOM elements How callbacks from props are handled: whether or not they were called, called with what arguments, etc. Events emitted What is not part of the public API for a React Component? Not Inputs ❌ Values from component state ❌ Component instance APIs: setState ❌ Lifecycle APIs: componentWillMount, etc. Not Outputs ❌ Internal state/state changes ❌ Lifecycle methods being called Testing Async Code Testing async code requires special care to ensure that all of the assertions actually run. Jest tests can be set up to fail if the number of received assertions doesn’t match the expected number with a special expectation: expect.assertions(count). Jest will also fail a test that returns a rejected Promise. If you are testing error handling behavior it is extra important to add the assertion count so that returning a resolved Promise does not make the test pass without running the .catch code and its assertions. Callback style with done it("should get item from API", done => { expect.assertions(1) // ⚠️ fetch('/url/1') .then(data => expect(data).toHaveLength(1); done) .catch(err => done) }) Returning a Promise it("should get item from API", () => { return fetch('/url/1') .then(data => expect(data).toHaveLength(1)) }) Using async/await syntax with Promises it("should get item from API", async () => { const data = await fetch('/url/1') expect(data).toHaveLength(1) }) Testing error handling with async/await it("should not get item when unauthorized", async () => { expect.assertions(1) // ⚠️ try { const data = await fetch('/url/2') } catch(err) { expect(err.message).toEqual('Not Authorized') } }) Testing Library Code Avoid writing tests that would be covered by a library’s own test suite. Functional/end-to-end tests against the app in a staging environment act as a sanity check against these assumptions, ensuring that both libraries and app code work. unit ❌ Verifying that a component receives new props after calling a Redux action (this just tests that connect works) ❌ Confirming that a function call is delayed using a utility library like Lodash _.debounce ❌ Checking that a React lifecycle method (i.e., componentDidMount) is called Quotes About Testing (Eric Elliot) What Every Unit Test Needs The tests should halt the delivery pipeline on failure and produce a good bug report when they fail. (Randy Coulman) Tautological Tests Never write test code that assumes it knows how the method under test should be implemented. See also (Fabio Pereira) Tautological Tests (Wikipedia) Behavior-Driven Development Behavior-driven development specifies that tests of any unit of software should be specified in terms of the desired behavior of the unit. The “desired behavior” in this case consists of the requirements set by the business.