Register to get access to free programming courses with interactive exercises

Data preparation JS: Automated testing

Most tests on a given feature are very similar to each other, especially in terms of initial data preparation. In the last lesson, each test started with the line makeStack(). We're not duplicating things just yet, but we're making steps towards it. Generally, actual tests are more complicated and involve a ton of preparatory work.

Imagine we're working on the Lodash library and want to test its functions for processing collections:

  • find
  • filter
  • includes
  • and others (there are about 20 of them in total)

These functions require a pre-prepared collection to work. It is easiest to come up with one that's suitable for testing most or even all the functions:

import _ from 'lodash';
test('includes', () => {
  // We've prepared a collection, coll
  const coll = ['One', true, 3, 10, 'cat', {}, '', 10, false];

  // We'll use coll for testing
  expect(_.includes(coll, 3)).toBe(true);
  expect(_.includes(coll, 11)).toBe(false);
});

Now imagine that there are dozens of these tests (in fact, there are hundreds). The code will wander from place to place, generating more and more copies and pastes of itself.

The easiest way to avoid this is to move where the collection is defined to the module level, outside of the test functions:

import _ from 'lodash';

const coll = ['One', true, 3, 10, 'cat', {}, '', 10, false];

test('includes', () => {
  expect(_.includes(coll, 3)).toBe(true);
  expect(_.includes(coll, 11)).toBe(false);
});

This simple solution removes unnecessary duplication. Note, however, that it only works within a single module. Collections like this still have to be defined in each test module. And in our case, this is actually good for us.

The point is that any kind of overgeneralization that leads to a complete elimination of redundancy introduces implicit dependencies into the code. Changing this collection will almost certainly break most of the tests that are tied to its structure, as well as to the number of elements and their values:

import _ from 'lodash';

const coll = [1, 2, 3, 4];

test('filter', () => {
  // Choose only even-numbered ones
  expect(_.filter(coll, (v) => v % 2 === 0)).toEqual([2, 4])
});

The test above would break if we added another even number to our collection. And the collection will almost certainly have to be expanded when new tests are added (for this or other functions).

The main conclusion from this is we need to get rid of and avoid duplications. But it's important not to cross the line and allow generalization to start to hinder rather than help.

But it's not always possible to bring the constants to module level. This applies primarily to dynamic data. Imagine some code like this:

const now = Date.now(); // current timestamp

test('first example', () => console.log(now));
test('second example', () => console.log(now));

//  console.log __tests__/index.test.js:3
//    1583871515943
//
//  console.log __tests__/index.test.js:4
//    1583871515943

The catch here is that the module is loaded into memory exactly once. This means that all the code defined at the module level (including constants) is executed exactly once. For example, the constant now will be defined before all the tests are run, and only after will Jest start to carry out tests. And with each subsequent test, the lag between the value of the constant now and the current real value of now will be further and further away.

Why might that be a problem? A code that works with the concept of "now" can count on the fact that "now" is kind of a snapshot of a given moment in time. But in the example above, now begins to lag behind the real now, and the more tests and the more complex they are, the greater the lag.

It is important not to forget that the test function does not run the test. It adds it inside Jest, and Jest decides when to perform this test. Therefore, an indefinite amount of time passes between loading the module and running the tests.

To solve this problem, test frameworks provide hooks — special functions that run before or after tests. Below is an example of how to create a date before each test:

let now;

// Runs before each test
beforeEach(() => {
  now = Date.now(); // current timestamp
});

test('first example', () => console.log(now));
test('second example', () => console.log(now));

//  console.log __tests__/index.test.js:9
//    1583871515943
//
//  console.log __tests__/index.test.js:10
//    1583871515950

https://repl.it/@hexlet/js-testing-setup-globals-en#index.test.js

beforeEach(callback) takes the function performing an initialization. It doesn't necessarily create variables. Perhaps initialization is about preparing the file system, such as by creating files.

But if it has to create data and make them available in tests, you have to use variables defined at the module level. Since everything that's defined within a function (the callback in our case) stays inside that function.

Even if we need to execute the code once before all the tests, it still needs to be executed inside the hook beforeAll(callback). This hook is run exactly once before all the tests are located in a given module.

import fs from 'fs';

let fileData;

beforeAll(() => {
  fileData = fs.readFileSync('path/to/file');
});

// This kind of call at the module level (outside hooks) is generally considered the wrong approach
// fileData = fs.readFileSync('path/to/file');

Why does it matter? To answer this question, we need to know a little more about the asynchronous nature of JavaScript. This issue is dealt with later in the course, but for now, we can limit ourselves to this: Jest must control the processes and side effects in tests. Everything that's called at the module level is executed outside of Jest. This means that Jest has no way to keep track of what is happening and at what point you can run tests.


Recommended materials

  1. Jest methods

Are there any more questions? Ask them in the Discussion section.

The Hexlet support team or other students will answer you.

About Hexlet learning process

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.

Get access
130
courses
1000
exercises
2000+
hours of theory
3200
tests

Sign up

Programming courses for beginners and experienced developers. Start training for free

  • 130 courses, 2000+ hours of theory
  • 1000 practical tasks in a browser
  • 360 000 students
By sending this form, you agree to our Personal Policy and Service Conditions

Our graduates work in companies:

<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.bookmate">Bookmate</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.healthsamurai">Healthsamurai</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.dualboot">Dualboot</span>
<span class="translation_missing" title="translation missing: en.web.courses.lessons.registration.abbyy">Abbyy</span>
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time

Use Hexlet to the fullest extent!

  • Ask questions about the lesson
  • Test your knowledge in quizzes
  • Practice in your browser
  • Track your progress

Sign up or sign in

By sending this form, you agree to our Personal Policy and Service Conditions
Toto Image

Ask questions if you want to discuss a theory or an exercise. Hexlet Support Team and experienced community members can help find answers and solve a problem.