The operation of any application can be seen as a life cycle divided into three stages:
- Initialization
- Execution
- Completion
Depending on how these stages are implemented, the code may be easy to test and support, or complex and practically untestable. In this lesson, we'll look at the most important part – the difference between initialization and execution, we'll break down specific examples, and learn how to correctly separate responsibilities.
What is initialization? Before any application can work, all the necessary libraries and frameworks must be set up, linked together, and running. In backend development, it's a lot easier because the frameworks have full control over this process. In the frontend, a lot is left to the developer himself, so there are often situations where the initialization process is either not separated, or it's only partially separated, or otherwise unsuccessfully.
What does initialization include? Everything that needs to be done exactly once so it can be used later in the application:
- Creating an initial state
- Connecting WebSockets
- Setting up i18next
- Loading and running the framework, if there is one
- Setting up various libraries: http-clients, working with dates and so on
This list is far from complete, every situation is different and involves different things being initialized.
In practice, the initialization process is organized as follows: a function is created, in which all the necessary setup is done. You can do this in the init.js file for convenience.
// A hypothetical example
// init.js file
import i18next from 'i18next';
import io from 'socket.io-client';
// Initial function
export default async () => {
// creating an i18next instance
const i18nextInstance = i18next.createInstance();
await i18nextInstance.init({
lng: 'en',
resources: /* translations */
});
const state = {
/* status description */
}
// socket creation
const socket = new io();
socket.on(/* setting up WebSockets */);
const form = document.querySelector('some-form');
form.addEventListener('submit', (e) => {
// And here is the logic of the application. It's better to put it somewhere else.
// state and socket are used somewhere in these handlers
});
};
This function is run elsewhere, for example in the index.js file, which is the entry point into the application:
import runApp from './init.js';
runApp();
Why is it divided like this? In frontend development, applications are usually global. I.e., there's one application for the entire page, it's loaded exactly once and run exactly once, and it controls everything that happens as if there was nothing else around. But this isn't always the case. For example, widgets can appear on the same page more than once, which means that each widget must be its own thing. I.e., the initialization of such a widget works in its own environment with its own objects and doesn't change anything outside (global objects). Otherwise, there'll be conflicts and widgets will interfere with each other.
On the other hand, when it comes to testing, each test is constructed so that it's independent of other tests, i.e., each test works as if no other tests exist. This behavior requires initializing the application for each test from scratch. Only in this case can you ensure that changes made to the state of the application in one test will not affect the other tests. A prime example is the initialization of i18next. This library exports a global object that can only be initialized once, re-initializing the same object (e.g., by re-running the application in tests) will lead to bugs and is forbidden by the documentation. For this reason, in the example above, each time the application starts, it creates its own instance of i18next, which is then passed to the functions that use it.
// Tests are studied in detail in other courses
// Somewhere in the tests
import runApp from '../src/init.js';
// This function is performed before each test
beforeEach(() => {
runApp(); // initializing
});
test(/* tests */)
test(/* tests */)
In the example above, it's the function that initializes the application, not the index.js file, where the function is called. If we had imported the index.js file, the initialization would have happened immediately during the import, and in terms of testing it would have been global. In this case, normal testing is almost impossible.
Recommended materials
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.