Unit tests are very useful for checking how our app is working.
Otherwise, we run into all kinds of issues later on.
In this article, we’ll look at some best practices we should follow when writing JavaScript unit tests.
Don’t Test Multiple Concerns in the Same Test
We should only test one thing in each test to keep things simple.
It’ll help us locate any problems easier if we find issues.
For example, instead of writing:
it('should save profile data and update the view with profile data', () => {
// expect(...)to(...);
// expect(...)to(...);
});
We write:
it('should save profile data', () => {
// expect(...)to(...);
});
it('should update the view with profile data', () => {
// expect(...)to(...);
});
Writing ‘and’ or ‘or’ in the test description means that we should break them up into multiple tests.
Cover the General Case and the Edge Cases
We got to cover edge cases so that we won’t miss anything.
They’re easy to miss but people will notice them.
For instance, instead of writing:”
it('should properly calculate arithmetic expression', () => {
const result = calculate('(5 + 10) / 2');
expect(result).toBe(7.5);
});
We write:
describe('The arithmetic expression calculator', () => {
it('should return null when the expression is an empty string', () => {
const result = calculator('');
expect(result).toBeNull();
});
it('should return value if expression is the value', () => {
const result = calculator('12');
expect(result).toBe(12);
});
it('should properly calculate arithmetic expression', () => {
const result = calculate('(5 + 10) / 2');
expect(result).toBe(7.5);
});
it('should throw an error whenever an invalid expression is passed', () => {
const calc = () => calculator('1 + 1 -');
expect(calc).toThrow();
});
});
Always Start by Writing the Simplest Failing Test with TDD
We should always write a failing test first with TDD.
Then we can build our functionality to make it pass.
Always Make Small Steps in Each Test-First Cycle
Making small steps in each test first cycle would let us work our way from failing to passing tests.
For instance, instead of writing:
it('should properly calculate arithmetic expression', () => {
const result = calculate('((5 + 10) / 2) + 10');
expect(result).toBe(17.5);
});
to test everything, we test small cases separately ane make each case pass:
describe('arithmetic expression calculator', () => {
it('should return null when the expression is an empty string', () => {
const result = calculator('');
expect(result).toBeNull();
});
it('should return value if the expression only has one value', () => {
const result = calculator('12');
expect(result).toBe(12);
});
describe('addition', () => {
it('should properly calculate a simple addition', () => {
const result = calculator('10 + 1');
expect(result).toBe(11);
});
it('should properly calculate a complex addition', () => {
const result = calculator('1 + 2 + 3');
expect(result).toBe(6);
});
});
// ...
describe('complex expressions', () => {
it('should properly calculate an expression containing all operators', () => {
const result = RPN('5 + (10 / 2) * 2 - 1'));
expect(result).toBe(14);
});
});
});
We go from the simplest cases to the most complex cases with our tests.
This way, we know we won’t miss much if anything.
Conclusion
We should test one thing in each test.
Also, we should have a test to test anything from the simplest to the most complex cases.