Testing
by Elizaveta Anatskaya
EXPECTED
ACTUAL
Why testing?
👌 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/BDD
👌 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.
TDD/BDD
TDD/BDD
What to test?
Testing Pyramid
👌 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.
Test types
-
👌 Functional testing is using software to check if its behavior matches the expectations. Expectations should be documented in the form of a technical spec which can be written in the form of a documentation or a user story. Functional testing applies to all levels of tests from unit to end-to-end. If it answers the “what” question, it checks if a function returned the expected value.
-
👌 Nonfunctional testing focuses on usability, reliability, maintainability and other attributes. It answers the “how” question, for example function’s performance when it returns a specific value
-
👌 Regression testing checks if new changes to the system have not broken existing functionality or caused old bugs to reappear. It’s done by re-running all tests on the system.
-
👌 Smoke testing focuses only on the most important functions work.
-
👌 Static analysis is a type of testing where code is analyzed for any errors without running or executing the code. The most common form of static analysis are linters.
-
👌 Performance testing
- 👌 Security testing
So maybe frontend needs a different approach to testing?
👌 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.
Why Jest and Enzyme
👌 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.
Setting up Jest and Enzyme
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"]
}
}
Creating our first test
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
Running tests
You’ll see something like this:
mount() vs shallow() vs render()
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.
Snapshot testing
Text
Snapshot testing sounds like a good idea, but has several problems:
- easy to commit snapshots with bugs;
- failures are hard to understand;
- a small change can lead to hundreds of failed snapshots;
- we tend to update snapshots without thinking;
- coupling with low-level modules;
- test intentions are hard to understand;
- they give a false sense of security.
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!`);
});
To simulate() or not
There are two ways to fire an event in Enzyme:
- using simulate() method, like wrapper.simulate('click');
- calling an event handler prop directly, like wrapper.props().onClick().
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.
Testing React components: rendering
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.
Testing React components: user interaction
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);
});
Testing React components: event handlers
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
});
});
Testing React components: async tests
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.
Mocking
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);
});
});
});
Mocking the fetch API
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));
});
});
Let's practice
See you!
bye
Testing
By Elizabeth Anatskaya
Testing
- 526