Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
Contents
Pt 1: Why write tests?
Pt 2: Testing practices
Pt 3: Testing React
Pt 4: More testing practices
Why write tests?
- To prove your code works
- To protect your code from breaking when you work on it
- To document behavior
- To help make design decisions
To prove your code works
Tests should capture the intent of a feature by describing 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 code from breaking when you work on it
A comprehensive test suite allows you to move quickly by verifying that code that used to work still works.
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 (TDD) advocates recommend writing tests before code: Write a test, then write just the code to make it pass; 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.
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
- Jest — test runner, assertions, mocks
- Update 2018–07–02: I now recommend react-testing-library over Enzyme for testing components
- Sinon — stubs and mocks (Jest has built-ins for some of this functionality so you might not need it)
yarn add --dev jest react-testing-library sinon
Render components:
// importing renderers for snapshotting and DOM assertionsimport { 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 “black box” that takes inputs and returns outputs; the test should verify that, given a certain input or combination of inputs, the correct output comes out.
What is the public API for a React Component?
Inputs
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 doneit("should get item from API", done => {expect.assertions(1) // ⚠️ fetch('/url/1') .then(data => expect(data).toHaveLength(1); done) .catch(err => done)})
Returning a Promiseit("should get item from API", () => {return fetch('/url/1') .then(data => expect(data).toHaveLength(1))})
Using async/await syntax with Promisesit("should get item from API", async () => { const data = await fetch('/url/1') expect(data).toHaveLength(1)})
Testing error handling with async/awaitit("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 unit 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.
- ❌ 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
What Every Unit Test Needs (Eric Elliot)
The tests should halt the delivery pipeline on failure and produce a good bug report when they fail.
Tautological Tests (Randy Coulman)
Never write test code that assumes it knows how the method under test should be implemented.
See also Tautological Tests (Fabio Pereira)
Behavior-Driven Development (Wikipedia)
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.
Guidelines for testing React components was originally published in Hacker Noon on Medium, where people are continuing the conversation by highlighting and responding to this story.
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.