Register to get access to free programming courses with interactive exercises

Dependency Inversion JS: Advanced Testing

It's not always the case that the result of a function is associated with a side effect, even though it was in the previous lesson. Sometimes, a side effect is just an extra action that interferes with the testing of the main logic, instead of helping.

Imagine a function that registers a user. It creates an entry in the database and sends a welcome email:

const params = {
  email: 'lala@example.com',
  password: 'qwerty',
};
registerUser(params);

This feature does many things, but the main thing we care about is registering the user correctly. Typically, registration boils down to adding a record of a new user to the database. This is what you need to check: you need to check that a new record has appeared in the database with the data filled in correctly. But returning the function won't help us in any way.

As a rule, when it comes to testing, databases aren't hidden. In web frameworks, it's available in the test environment and works as usual. It achieves idempotency through transactions. The transaction starts before the test and rolls back after the test. Because of this, each test runs in an identical environment and it doesn't matter how it changes it:

// A hypothetical example
const ctx = /* connect to db */;
beforeEach(() => ctx.beginTransaction());

test('registerUser', () => {
  // Inside we see data being added to the database
  const id = registerUser({ name: 'Mike' });
  const user = User.find(id);
  expect(user).toHaveProperty('name', 'Mike');
})

// The base returns to its original state using rollback
afterEach(() => ctx.rollbackTransaction());

But it's more complicated when it comes to sending emails. It definitely can't be done, but how can it be done? Take a look at what user registration might roughly look like:

import sendEmail from './emailSender.js';
const registerUser = (params) => {
  const user = new User(params);
  if (user.save()) {
    sendEmail('registration', { user });
    return true;
  }
  return false;
}

There are several approaches to disable sending in tests. The simplest one is the environment variable, which shows the execution environment:

// Execute this code only if we're not in a test environment
if (process.env.NODE_ENV !== 'test') {
  sendEmail('registration', { user });
}

Although it's easy to use, this approach is considered bad practice. Technically, this causes a violation of abstraction; the code begins to know about where it is executed. Over time there are more and more of these checks and the code gets messier. Moreover, if we still need to make sure that the email is sent (with the correct data!), we can't.

The next way is to support test mode within the library itself. For example, somewhere in the initialization phase of the tests, you could do this:

// setup.js in jest
import sendEmail from './emailSender.js';

// This approach has many variations, starting with flagging,
// and ending with replacing the functions in the prototype.
sendEmail.test = true;

Now, no real sending will take place in any other place we import and use sendEmail():

// Nothing happens
sendEmail('registration', { user });
// Unlike the first variant, the application code doesn't guess anything

This is a fairly popular solution. Usually, information on how to properly enable test mode can be found in the official documentation of the specific library in question.

What do you do if the library you're using doesn't support test mode? There is another, much more universal way. It is based on the application of dependency inversion. You can change the program so that it doesn't call sendEmail() directly, but rather takes it as a parameter:

import sendEmail from './emailSender.js';

// We set the default value so that we don't have to enter the function all the time
const registerUser = (params, send = sendEmail) => {
  const user = new User(params);
  if (user.save()) {
    send('registration', { user });
    return true;
  }
  return false;
}

And the test:

const fakeSendEmail = (...args) => {
  /* For example, the email can be displayed in the terminal for easy debugging  */
};

test('registerUser', () => {
  const id = registerUser({ name: 'Mike' }, fakeSendEmail);
  const user = User.find(id);
  expect(user).toHaveProperty('name', 'Mike');
});

This method is more difficult to implement, especially if the function is deep in the call stack. This means that you have to thread the necessary dependencies through the whole chain of functions from top to bottom. There can be many dependencies themselves, and the more inversion is used, the more complex the code. There's a price to pay for flexibility.

Now the pros. Neither the library nor the code knows anything about the tests. This method is the most flexible, it allows you to set a specific behavior for a particular situation. In some ecosystems, dependency inversion defines the application build process. Especially in the world of PHP, Java and C#.


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.

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:

Bookmate
Health Samurai
Dualboot
ABBYY
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.