JS: Advanced Testing
Theory: Dependency Inversion
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:
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:
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:
There are several approaches to disable sending in tests. The simplest one is the environment variable, which shows the execution environment:
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:
Now, no real sending will take place in any other place we import and use sendEmail():
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:
And the test:
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#.

