A Basic Introduction to Testing React applications
This blog is for absolute beginners, and here I am going to cover the building blocks of testing react applications using Jest and ‘react-testing-library’
Test Runner
A test file usually ends with .test.js or .spec.js and it can have one or more tests. Test Runner will list all the test files available in the project and then test and display the pass-fail status for each. We can ofcourse restrict the test runner to handle just one test file or a bunch of test files.
Writing a test
Writing a test, has two parts.
- First we need to tell in a sentence or phrase, what we are going to test. Infact this also serves as a low-level technical requirement documentation.
- And then we need to have the test function which have the code to programatically test it.
The test will fail if the test function throws an error and the test passes if it does not throw an error.
test('A sample test that fails', () => {
if(2+3 !== 4){
throw new Error('failed!');
}
})
test('A sample test that passes', () => {
if(2+2 !== 4){
throw new Error('failed!');
}
})
test('An empty test function always passes', () => {});
Assertion library
Assertion library helps us to write easily readable test cases. Though the above tests are valid, they are not easily readable. Compare it with the below code.
test('A sample test that fails', () => {
expect(2+3).toBe(4);
})
test('A sample test that passes', () => {
expect(2+2).toBe(4);
})
Jest comes with an assertion library. But in case if we use other frameworks we may need to use two libraries in combination. For example, we can use Mocha and Chai, where Mocha it the test runner and Chai is the assertion library.
Testing is classified into three types: unit testing, integration testing and End to End testing. While writing tests, being practical and pragmatic is very important. We should not try to cover all possibilities, because writing tests take time. Another concern is, in agile and start up environments, the requirements may change often, and the test written will become obsolete. So we need to have some guidelines and stratergy is writing tests.
The starting point is to test functions that runs independently like utility functions. It takes some input and after processing it, it gives an output. It is easy to write test for these cases.
test('test transform function', () => {
expect(transform(input)).toBe(output);
}
Then we can write test around the data models. Sometimes when the data model changes, we handle the changes in few places and forget at few places. Having tests around them will ensure that we handle at all places.
And finally and most importantly we need to test the DOM interactions and maniputations. And this is the tricker part for a front-end developer.
We can write tests for DOM using reactDOM library. Lets say we have a simple counter component.
const Counter = ({ count, onIncrement }) => (
<div>
<span>{count}</span>
<button onClick={onIncrement}>+</button>
</div>
);
Find below the test using reactDOM library.
test('test Counter component', () => {
const container = document.createElement('div');
ReactTestUtils.render(
<Counter count={5} onIncrement={() => {}} />,
container
);
const incrementBtn = container.querySelector('button');
expect(incrementBtn.textContent).toBe('+');
const countDom = container.querySelector('span');
expect(countDom.textContent).toBe('5');
});
Here we are testing that the component is rendered properly and the html elements have proper labels. But these are just basic tests and we need to test more. We need to simulate user actions like button clicks, state and props changes.
But by using reactDOM, the possibilities are limited. So we can use ‘react-testing-library’. It is a minimalistic library and it is easy to write and maintain tests.
import { render, fireEvent, cleanup } from 'react-testing-library';
...
afterEach(cleanup);
test('test Counter component with react-testing-library', () => {
let count = 5;
const onIncrement = jest.fn(() => {
count++;
});
const { getByText, container, rerender } = render(
<Counter count={count} onIncrement={onIncrement} />
);
fireEvent.click(getByText(/\+/));
expect(onIncrement).toHaveBeenCalledTimes(1);
let label = container.querySelector('label');
expect(label.textContent).toBe('5');
rerender(<Counter count={count} onIncrement={onIncrement} />);
expect(label.textContent).toBe('6');
});
In the above code, we can test a lot of interesting stuff.
- We are simulating the click event of the button, by querying the text present in the button.
- Using a mock function (onIncrement) we are checking whether the listener function is getting called on click of the button.
- We are rerendering the component and checking whether the props was updated properly in the label.
Note: Rerender will not recreate the component. It it evident, since we are still able to use the queried label variable even after the rerender and we get the updated value.
If you have any questions, or find any mistakes, please add it in the comment.