Testing
by Elizaveta Anatskaya
👌 Confidence to change code: well-written tests allow you to refactor code with confidence that you’re not breaking anything, and without wasting time updating the tests.
Automated tests make it possible to catch bugs before you commit them to the repository, in comparison to manual testing where you find most of the bugs during testing or even in production.
👌 Documentation: tests explain how code works and what’s the expected behavior. Tests, in comparison to any written documentation, are always up to date.
👌 Bugs and regression prevention: by adding test cases for every bug, found in your app, you can be sure that these bugs will never come back. Writing tests will improve your understanding of the code and the requirements, you’ll critically look at your code and find issues that you’d miss otherwise.
👌 TDD = Test Driven Development
is a software development methodology whereby you write and run a set of tests before you write code.
👌 BDD - given and when then = Behavior Driven Development
is a software development method that focuses on creating tests using concrete, real-life examples. These examples use natural language constructs (English-like sentences) to express the behavior and the expected outcomes. Actually BDD is an extension of TDD approach but it used for test the behavior but not the concrete realization of the function. It’s especially helpful when you’re working with a cross-functional team. And allow you to involve both technical and non-technical team members in defining how your software should behave.
👌 UI tests = E2E test the whole app loaded in a real browser, usually with a real database. It's the only way to ensure that all parts of your app work together, but they are slow, tricky to write and often flaky.
👌 Service tests are somewhere in the middle: they test integration of multiple units but without any UI.
👌 Unit tests are testing a single unit of code, like a function or a React component. You don’t need a browser or a database to run unit tests, so they are very fast.
👌 give you confidence that all features of your app work as expected. Tools: Jest and Enzyme or react-testing-library.
👌 verify that tricky algorithms work correctly. Tools: Jest.
👌 catches syntax errors, bad practices and incorrect use of APIs: — Code formatters, like Prettier;
— Linters, like ESLint;
— Type checkers, like TypeScript and Flow.
👌 make sure that your app work as a whole: the frontend and the backend and the database and everything else. Tools: Cypress.
👌 Very fast.
👌 Interactive watch mode that only runs tests which are relevant to your changes.
👌 Helpful failure messages.
👌 Simple configuration, or even zero configuration.
👌 Mocks and spies.
👌 Coverage reports.
We’ll set up and use these tools:
Jest has many benefits over other test runners:
Text
Some of the Enzyme pros are:
👌 gives you jQuery-like API to find elements, trigger event handler, and so on
👌 access the DOM
👌 shallow rendered tests run faster.
Enzyme adds up perfectly to Jest, because it can cover unit and integration tests, whereas Jest is mainly used for snapshot tests.
npm install --save-dev jest react-test-renderer enzyme enzyme-adapter-react-16 node-fetch
Create a src/setupTests.js file to customize the Jest environment:
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// Configure Enzyme with React 16 adapter
Enzyme.configure({ adapter: new Adapter() });
// If you're using the fetch API
import fetch from 'node-fetch';
global.fetch = fetch;
Then update your package.json like this:
{
"name": "pizza",
"version": "1.0.0",
"dependencies": {
"react": "16.8.3",
"react-dom": "16.8.3"
},
"devDependencies": {
"enzyme": "3.9.0",
"enzyme-adapter-react-16": "1.11.2",
"jest": "24.6.0",
"node-fetch": "2.6.0",
"react-test-renderer": "16.8.6"
},
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"setupFilesAfterEnv": ["<rootDir>/src/setupTests.js"]
}
}
import React from 'react';
import { mount } from 'enzyme';
test('hello world', () => {
const wrapper = mount(<p>Hello Jest!</p>);
expect(wrapper.text()).toMatch('Hello Jest!');
});
Run npm run test to run all tests.
Run npm run test:watch to run Jest in watch mode
Enzyme has three rendering methods:
👌 mount() renders the whole DOM tree and gives you jQuery-like API to access DOM elements inside this tree, simulate events and read text content. I prefer this method most of the time.
👌 render() returns a string with rendered HTML code, similar to the renderToString() method from react-dom. It’s useful when you need to test HTML output. For example, a component that renders Markdown.
👌 shallow() renders only the component itself without its children.
Text
Snapshot testing sounds like a good idea, but has several problems:
Avoid snapshot testing unless you’re testing very short output with clear intent, like class names or error messages, or when you really want to verify that the output is the same.
If you use snapshots keep them short and prefer toMatchInlineSnapshot() over toMatchSnapshot().
test('shows out of cheese error message', () => {
const wrapper = mount(<Pizza />);
expect(wrapper.debug()).toMatchSnapshot();
});
test('shows out of cheese error message', () => {
const wrapper = mount(<Pizza />);
const error = wrapper.find('[data-testid="errorMessage"]').text();
expect(error).toMatchInlineSnapshot(`Error: Out of cheese!`);
});
There are two ways to fire an event in Enzyme:
Which method to use is a big debate in the Enzyme community.
The name simulate() is misleading: it doesn’t really simulate an event but calls the prop the same way we’d do it manually. These two lines will do almost the same:
wrapper.simulate('click'); wrapper.props().onClick();
Most of the time difference between calling an event handler directly (either by calling a prop or with simulate() method) and the real browser behavior isn’t important but in some cases this difference may lead you to misunderstanding of your tests’ behavior. For example, if you simulate() a click on a submit button in a form, it won’t submit the form, like a real submit button would do.
Text
This kind of test can be useful when your component has several variations and you want to test that a certain prop renders the correct variation.
import React from 'react';
import { mount } from 'enzyme';
import Pizza from '../Pizza';
test('contains all ingredients', () => {
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples'];
const wrapper = mount(<Pizza ingredients={ingredients} />);
ingredients.forEach(ingredient => {
expect(wrapper.text()).toMatch(ingredient);
});
});
Here we’re testing that Pizza component renders all ingredients passed to a component as a prop.
Text
To “simulate” an event like click or change, call the simulate method and then test the output:
import React from 'react';
import { mount } from 'enzyme';
import ExpandCollapse from '../ExpandCollapse';
test('button expands and collapses the content', () => {
const children = 'Hello world';
const wrapper = mount(
<ExpandCollapse excerpt="Information about dogs">
{children}
</ExpandCollapse>
);
expect(wrapper.text()).not.toMatch(children);
wrapper.find({ children: 'Expand' }).simulate('click');
expect(wrapper.text()).toMatch(children);
wrapper.update();
wrapper.find({ children: 'Collapse' }).simulate('click');
expect(wrapper.text()).not.toMatch(children);
});
Text
When you unit test a single component, event handlers are often defined in the parent component, and there are no visible changes as a reaction to these events. They also define the API of a component that you want to test.
jest.fn() creates a mock function, or a spy, that allows you to check how many times it was called and with which parameters.
import React from 'react';
import { mount } from 'enzyme';
import Login from '../Login';
test('submits username and password', () => {
const username = 'me';
const password = 'please';
const onSubmit = jest.fn();
const wrapper = mount(<Login onSubmit={onSubmit} />);
wrapper
.find({ 'data-testid': 'loginForm-username' })
.simulate('change', { target: { value: username } });
wrapper
.find({ 'data-testid': 'loginForm-password' })
.simulate('change', { target: { value: password } });
wrapper.update();
wrapper.find({ 'data-testid': 'loginForm' }).simulate('submit', {
preventDefault: () => {}
});
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledWith({
username,
password
});
});
This approach is problematic. The delay will always be a random number. A number that is good enough on a developer’s machine at the time of writing the code. But it can be too long or too short at any other time and on any other machine. When it’s too long, our test will run longer than necessary. When it’s too short, our test will break.
const wait = (time = 0) =>
new Promise(resolve => {
setTimeout(resolve, time);
});
test('something async', async () => {
// Run an async operation...
await wait(100).then(() => {
expect(wrapper.text()).toMatch('Done!');
});
});
A better approach would be polling: waiting for the desired result, like new text on a page, by checking it multiple times with short intervals, until the expectation is true. The wait-for-expect library does exactly that:
import waitForExpect from 'wait-for-expect';
test('something async', async () => {
expect.assertions(1);
// Run an async operation...
await waitForExpect(() => {
expect(wrapper.text()).toMatch('Done!');
});
});
expect.assertions() method is useful for writing async tests: you tell Jest how many assertions you have in your test, and if you mess up something, like forget to return a Promise from test(), this test will fail.
With jest.mock() you can mock any JavaScript module. To make it work in our case, we need to extract our fetching function to a separate module, often called a service module:
export const fetchIngredients = () =>
fetch(
'https://httpbin.org/anything?ingredients=bacon&ingredients=mozzarella&ingredients=pineapples'
).then(r => r.json());
Then import it in a component:
import React from 'react';
import { fetchIngredients } from '../services';
export default function RemotePizza() {
/* Same as above */
}
And now we can mock it in our test:
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import waitForExpect from 'wait-for-expect';
import RemotePizza from '../RemotePizza';
import { fetchIngredients } from '../../services';
jest.mock('../../services');
afterEach(() => {
fetchIngredients.mockReset();
});
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples'];
test('download ingredients from internets', async () => {
expect.assertions(4);
fetchIngredients.mockResolvedValue({ args: { ingredients } });
const wrapper = mount(<RemotePizza />);
await act(async () => {
wrapper.find({ children: 'Cook' }).simulate('click');
});
await waitForExpect(() => {
wrapper.update();
ingredients.forEach(ingredient => {
expect(wrapper.text()).toMatch(ingredient);
});
});
});
It is similar to mocking a method, but instead of importing a method and mocking it with jest.mock(), you’re matching a URL and giving a mock response.
We’ll use fetch-mock to mock the API request:
import React from 'react';
import { mount } from 'enzyme';
import fetchMock from 'fetch-mock';
import { act } from 'react-dom/test-utils';
import waitForExpect from 'wait-for-expect';
import RemotePizza from '../RemotePizza';
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples'];
afterAll(() => fetchMock.restore());
test('download ingredients from internets', async () => {
expect.assertions(4);
fetchMock.restore().mock(/https:\/\/httpbin.org\/anything\?.*/, {
body: { args: { ingredients } }
});
const wrapper = mount(<RemotePizza />);
await act(async () => wrapper.find({ children: 'Cook' }).simulate('click'));
await waitForExpect(() => {
wrapper.update();
ingredients.forEach(ingredient =>expect(wrapper.text()).toMatch(ingredient));
});
});
bye