Snapshots provide an easy way to show how React components are rendered. Any
time a component is updated, its snapshot needs to be regenerated. This can be
done by running yarn test -u
.
The updated snapshot should be manually inspected by both the author and code reviewer to ensure that the changes that were made are intentional and correct.
Care should be taken when writing snapshot tests, as they fail quite easily, and only show that something has changed. Generally one snapshot test for a component is pretty good, while the larger component behavior should be asserted using more specific expectations.
We use React Testing Library to tests our React components. Generally we try to test our components by exercising them just like a user would do: finding a control by text using the queries provided by the library and fire events to these targets.
In other cases, especially connected components that react to state changes produced by other components, we dispatch Redux actions and test our expectation afterwards.
Most of our tests use a setup
function. Its role is to setup the environment
(create a proper profile, dispatch actions to get a good state, render a
component), and define higher-level functions to manipulate the component. Then
the returned object contains these functions along with useful values that tests
can make use of.
Here is a full example:
describe('app/Details', function () {
function setup() {
const { profile } = getProfileFromTextSamples(`
A A A
B B B
C C H
D F I
E E
`);
const store = storeWithProfile(profile);
const renderResult = render(
<Provider store={store}>
<Details />
</Provider>
);
const { getByText } = renderResult;
function interactWithComponent() {
fireEvent.click(getByText(/Click me/));
}
return { ...renderResult, store, interactWithComponent };
}
it('reacts when interacted', () => {
const { container, interactWithComponent } = setup();
interactWithComponent();
expect(container.firstChild).toMatchSnapshot();
});
});
React Testing Library relies a lot on the underlying DOM library. In our case we use jsdom which is excellent but has a few shortcomings.
The event object MouseEvent
lacks some properties that we use, namely offsetX
,
offsetY
, pageX
, pageY
. We have a utility called getMouseEvent
that we
can use in this case:
import { getMouseEvent } from '../fixtures/utils';
...
// By using `fireEvent` directly and not one of its methods, we get to pass
// a full Event object.
fireEvent(target, getMouseEvent({ pageX: 5 }));
A lot of our components use the Canvas Context API to draw graphs and display data. To test this properly we developed a mock that can be used in this way:
import { autoMockCanvasContext } from '../fixtures/mocks/canvas-context';
...
autoMockCanvasContext();
it('draws the right things to the screen', () => {
expect(window.__flushDrawLog()).toMatchSnapshot();
});
The render
calls return a useful utility debug
that can be called in tests,
that will output the result of the render to the console. This is extremely
useful to better know how to target elements. This utility is also returned by
all setup
functions, so it's very easy to use it when needed:
it('renders a lot of things', () => {
const { container, debug } = setup()
debug();
...
});