- Tests affecting each other
- Conditional constructions in tests
- Tests outside of tests
- Too much detail
- Deep nesting
- Code with tests takes longer to write than code without tests
Tests, like any other code, can be written in different ways, one of those ways is badly. In addition to some common practices and coding standards, tests have their own peculiarities that you should be aware of. In this lesson, we'll go over some of them.
Tests affecting each other
One of the key rules is that tests must not affect each other. This means that any test is performed as if no other tests existed.
It's very easy to break this rule. A single test can create a file, change a variable, or write something to a database. If the rest of the tests come across these changes, they may fail when they shouldn't fail, or, conversely, pass when they shouldn't. In addition, such a situation can introduce uncertainty. Tests can occasionally fail for no apparent reason. For example, when a test runs in isolation, it works, but when it runs with others, it crashes:
let user;
test('first', () => {
user = { name: 'John' };
// ...
});
test('second', () => {
// The user that the other test created is used!
// This test depends on how the previous test works,
// and cannot work unless you run both tests in sequence
user.name = 'Peter';
});
This situation can happen particularly often in tests that actively interact with the things outside the program, such as databases or file systems. Testing side effects has its own tricky moments, we'll look at it more in the advanced testing course.
Conditional constructions in tests
test('something', () => {
if (/* something */) {
// Execute the code in one way
// The check may be here
} else {
// Execute the code different way
// The check may be here
}
// The check may be here
});
Any branching within tests is actually several tests within one test. You should get rid of that and never write like that.
Tests outside of tests
The goal of beforeEach
is to prepare the data and environment for testing, while the goal of test
is to call the code being tested and perform tests. But sometimes developers overdo it:
let result;
beforeEach(() => {
// The code being tested is called. This contradicts the idea of beforeEach.
result = sum(5, 9);
});
test('result', () => {
// Here it's only checking
expect(result).toEqual(14);
});
In this example, the code being tested is called in beforeEach
. This approach makes it difficult to analyze the tests because it turns everything upside down.
Too much detail
Programmers, influenced by advices from the Internet, tend to spread the code as much as possible across files, modules, and functions. The same is observed in testing. Instead of one test, which contains all the necessary checks, the programmer creates 5 tests, each of which contains exactly one check:
test('create user', () => {
const user = { name: 'Mark', age: 28 };
// Here's the code to add the user to the database
expect(user.age).toEqual(28);
});
test('create user 2', () => {
const user = { name: 'Mark', age: 28 };
// Here's the code to add the user to the database
expect(user.name).toEqual('Mark');
});
More often than not, this separation results in more code, and refactoring, later on, will be more complex because of the amount of code.
Deep nesting
Jest allows you to group tests into describe
blocks:
describe('User', () => {
test('should be valid', () => { /* ... */ });
});
They help you structure complex tests and assign each describe
block to its own beforeEach
. Although this feature can be useful, it's very easy to start using it to your detriment:
describe('', () => {
describe('...', () => {
describe('...', () => {
test('should be valid', () => { /* ... */ })
});
});
});
An entrenched test hierarchy makes them difficult to analyze and keeps them bonded to one another. This complicates adding new checks. It makes it unclear what each test refers to. This is the problem with any hierarchies that view the system from only one point of view.
Code with tests takes longer to write than code without tests
This is a rather crucial topic, which you can use to test how good a programmer is at writing tests. Although some kinds of tests are quite complex and require extra time, the ordinary tests that are written along the code help speed up development. And there are five reasons for that:
- Tests affect code design. They help identify bad decisions much earlier
- Preparing input data can take a considerable amount of time. If you test as you go along, you only need to do something once
- Checking the result of the code can be complex and varied. Tests mean that you don't have to worry about it, they do the checking themselves to make sure everything is okay, including borderline cases
- If you write tests for you projects regularly, it's easier and faster to refactor, since you don't have to check other parts of the code manually
- Tests reduce the anxiety of you doing something wrong
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.