React Unit Testing Notes
These notes are a guide Iâve written throughout coding the initial part of the application. The note starts out with fundamentals and continues with specific testing edge cases.
Philosophy
Write tests. Not too many. Mostly integration. Guillermo Rauch
The more your tests resemble the way your software is used, the more confidence they can give you. Kent C. Dodds
This project focuses mainly on integration tests. Why? We shouldnât mock too much as the tests themselves become unmaintainable. When you make any changes to the code with tests that have a lot of mocking, the tests also have to be updated. Mostly manual. And we end up creating more work for the developer than is actually worth.
Code coverage also isnât the best factor to aim for. Yes, we should have tests to cover our code. No, we shouldnât aim for 100% coverage. Paretoâs law can apply here. For most cases, we expect few test to cover most use cases. At some point, thereâs diminishing returns.
When to write a test
On more about testing philosophy, read Kent C. Doddâs post of the first quote: âWrite tests. Not too many. Mostly integration.â.
Effective Snapshot Testing
Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly. Jest documentation on snapshot testing.
To maximize snapshot testing, we have it for most components. The idea is to remove shallow testing (using enzyme) with rendering components.
Effective Snapshot Testing is a fantastic read for an intro.
Running Tests
Out of the box, the testing framework and its tools are installed with dependencies. For more information, checkout the installation section of the README.
Unit tests are run before a building the Docker container. Tests are run with Jest, that has the Expect expectations library given. As mentioned in the testing philosophy, we try not to focus on mocking. Sometimes this is inevitable and we have included Enzyme for shallow rendering.
Use shallow
sparingly. For more, read this article.
yarn test
Additional Commands
If there are any jest
flags you want to add to your tests, like watch mode or coverage, you can add those flags to the command.
Watch
# Run tests in watch mode
yarn test --watch
Coverage
# Run a coverage report
yarn test --coverage
# This will build a `coverage` folder that can be viewed for a full coverage report
Single file or folder
# Run tests over a single file
yarn test src/path/to/file
# Run tests over a folder
yarn test src/path/to/folder
State Management Testing
Test all actions, sagas, and reducers.
- Action tests are ensuring the action creators create the proper actions
- Reducer tests are ensuring the state has been changed properly
- Saga tests are more for E2E testing, making sure all side-effects are accounted for
Apollo Testing
Before continuing to this section, make sure youâre familiar with the (docs)[https://www.apollographql.com/docs/react/recipes/testing].
Known Warnings
React v16.9
Warning: componentWillReceiveProps has been renamed, and is not recommended for use. See https://fb.me/react-async-component-lifecycle-hooks for details.
- Move data fetching code or side effects to componentDidUpdate.
- If youâre updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://fb.me/react-derived-state
- Rename componentWillReceiveProps to UNSAFEcomponentWillReceiveProps to suppress this warning in non-strict mode. In React 17.x, only the UNSAFE name will work. To rename all deprecated lifecycles to their new names, you can run
npx react-codemod rename-unsafe-lifecycles
in your project source folder. Please update the following components: *
With a move to React v16.8 -> v16.9, componentWillMount
, componentWillReceiveProps
, and componentWillUpdate
lifecycle methods have been renamed.
They will be deemed unsafe to use. Our library has updated already, but some libraries may still use this.
Known libraries with issues:
- react-dates
- react-outside-click-handler (dev dependency to react-dates)
Common Test Errors
Redux Error
Invariant Violation: Could not find âstoreâ in the context of âConnect(Form(Form))â. Either wrap the root component in a âProviderâ, or pass a custom React context provider to âProviderâ and the corresponding React context consumer to Connect(Form(Form)) in connect options.
Solution
- Add imports
import { Provider } from "react-redux";
import configureStore from "redux-mock-store";
- Create the mock store. Wrap renderer with provider.
it("renders redux connected component", () => {
const mockStore = configureStore();
const store = mockStore({ form: {} });
const tree = renderer
.create(
<Provider store={store}>
<Component />
</Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
Thunk error
Youâve included redux in your test, but you might get the following message.
[redux-saga-thunk] There is no thunk state on reducer
If this is the case, go back to your mock store and include thunk
has a key.
it("renders a component that needs to thunk", () => {
const mockStore = configureStore();
const store = mockStore({ thunk: {} }); // Be sure to include this line with the thunking
const tree = renderer
.create(
<Provider store={store}>
<TestedComponent />
</Provider>
)
.toJSON();
expect(tree).toMatchSnapshot();
});
i18n Error
Sometimes, an i18n provider isnât given. The error doesnât appear to be useful.
TypeError: Cannot read property âreadyâ of null
Check if the component or a child component uses the Translation
component. If so, Translation requires context Provider
be wrapped around.
Solution
- Add imports
import { I18nextProvider } from "react-i18next";
import i18n from "../../../test-utils/i18n-test";
- Wrap renderer with the provider
const tree = renderer
.create(
<I18nextProvider i18n={i18n}>
<Component />
</I18nextProvider>
)
.toJSON();
- Rerun the test and check the snapshot. If the snapshot looks good, add the
-u
flag to update the snapshot.
Apollo Error
If the component requires an apollo component, you will want to pass in a mock provider.
Invariant Violation: Could not find âclientâ in the context or passed in as a prop. Wrap the root component in an âApolloProviderâ, or pass an ApolloClient instance in via props.
- Add imports
import { MockedProvider } from "@apollo/client/testing";
- Wrap renderer with the provider
const tree = renderer
.create(
<MockedProvider mocks={[]} addTypename={false}>
<Component />
</MockedProvider>
)
.toJSON();
- Rerun the test and check the snapshot. If the snapshot looks good, add the
-u
flag to update the snapshot.
In cases where the data is important, you will want to add mocks.
const mocks = [
{
request: {
query: ${query_name},
variables: ${variables}
},
result: {
data: ${result_data}
}
}
];
Query not wrapped in act(âŚ)
Warning: An update to Query inside a test was not wrapped in act(âŚ).
When testing, code that causes React state updates should be wrapped into act(âŚ):
act(() => { /_ fire events that update state / }); / assert on the output _/
This ensures that youâre testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
To fix, import the following:
import renderer, { act } from "react-test-renderer";
import wait from "waait";
The write code that looks like this.
it("renders something", async () => {
const tree = renderer.create(<SomeComponentWithApolloComponent />);
await act(async () => {
await wait(0);
});
expect(tree.toJSON()).toMatchSnapshot();
});
There may be a case you want to test the loading state. If so, use an async function, but do not add your act function.
it("renders something", async () => {
const tree = renderer.create(<SomeComponentWithApolloComponent />);
expect(tree.toJSON()).toMatchSnapshot();
});
To research: tree.update();
.
Resources
React Dates issue
TypeError: Cannot read property âcreateLTRâ of undefined
Solution
Solve by adding the following to the top of the test file
import "react-dates/initialize";
As of v13.0.0 of react-dates, this project relies on react-with-styles. If you want to continue using CSS stylesheets and classes, there is a little bit of extra set-up required to get things going. As such, you need to import react-dates/initialize to set up class names on our components. This import should go at the top of your application as you wonât be able to import any react-dates components without it.
Final Form
Warning: Field must be used inside of a ReactFinalForm component
- Add imports
import { Form } from "react-final-form";
- Wrap renderer with the Form component
const handleSubmit = jest.fn();
const tree = renderer
.create(
<Form onSubmit={handleSubmit}>
{(formProps) => <FormComponent {...formProps} />}
</Form>
)
.toJSON();
expect(tree).toMatchSnapshot();
Array Mutators
Array mutators not found. You need to provide the mutators from final-form-arrays to your form
In this case, add arrayMutators
from final-form-arrays
to the react-final-form
Form
component.
- Add imports
import arrayMutators from "final-form-arrays";
- Add to Form component
<Form
onSubmit={onSubmit}
mutators={{
...arrayMutators,
}}
>
{(formProps) => <FormComponent {...formProps} />}
</Form>
Prompt Error
Invariant failed: You should not use âPromptâ outside a âRouterâ
See Router Error for the solution
NavLink Error
Invariant failed: You should not use âNavLinkâ outside a âRouterâ
See Router Error for the solution
Router Error
Invariant Violation: You should not use âRouteâ or withRouter() outside a âRouterâ
Solution
- Add imports
import { StaticRouter } from "react-router";
- Wrap renderer with the provider
const tree = renderer
.create(
<StaticRouter context={{}}>
<Component />
</StaticRouter>
)
.toJSON();
expect(tree).toMatchSnapshot();
Hooks Errors
useEffect
Sometimes you rely on a useEffect
callback to initialize an effect.
const TestComponent = ({ specialProp }) => {
const doSomething = () => console.log("something");
useEffect(() => {
if (specialProp) {
doSomething(); // something
}
}, []);
return null;
};
When you use the test renderer, this wonât work. For an exhaustive way of triggering events, check out this post.
The preliminary solution is to run act
from the react-test-renderer
library.
Currently, there is no documentation to this, so itâs best to
read the code.
Hereâs how we use act.
it("creates component with useEffect", () => {
// Create your tree
const tree = renderer.create(
<TestComponentWithEffect>My Effect</TestComponentWithEffect>
);
// Tell the renderer to act, pushing the effect through
renderer.act(() => {});
expect(tree.toJSON()).toMatchSnapshot();
});
// Drawbacks:
// - Can't handle flushing (yet)
This will be revisited as the API matures.
Dealing with Time
If you need to mock time, you could use this implementation.
const constantDate = new Date("2019-05-16T04:00:00");
/* eslint no-global-assign:off */
Date = class extends Date {
constructor() {
super();
return constantDate;
}
};
Written by Jeremy Wong and published on .