Adding Observability to your Jest tests

Adding Observability to your Jest tests

Eliran Maman
Eliran Maman 8 Min Read
Learn how to write quality code in Node.JS

This blog will examine how to add OpenTelemetry Observability into your Jest testing frameworks. You’ll learn how to perform better testing using the most valuable packages. We’ll also explain why you should choose the framework as your project’s testing framework. And why we decided to add Observability to our Jest tests.

How to squeeze Jest for better performance

  • Run in-band – Run all tests serially in the current process, rather than creating a worker pool of child processes that run tests. This way, you can improve performance in low-resource environments using fewer resources and reducing the time dealing with multiple threads.

  • Watchman – Jest is a collection of packages, and one of the valuable packages is jest-haste-map. Jest provides static analysis to figure out how to run tests quickly; it creates a dependencies resolution (a time consumer) to detect which tests are affected by the latest changes (since the last time we invoked Jest). This package combs the project files and analyzes the files and the dependencies among them. To speed it up, Jest uses watchman (if available), which provides an excellent service by watching the code base and providing Jest with a list of changes to the project’s filesystem since the last invocation. This process allows Jest to analyze only the files which changed from the previous request. Using watchman will speed up Jest; if you don’t have watchman installed, jest-haste-map will search (using node.js on Windows / find on Linux) for all files and analyze all dependencies each time you will invoke Jest.

  • Number of workers – Here, you can find your optimal number of workers. Jest will use the number of available CPUs as max workers (meaning Jest will use up the number of available CPUs threads), using one thread for the CLI process and the rest for test workers (in watch mode, it will be half of the available CPUs). Sometimes less is more, and using more workers does not always result in better performance, especially when talking about a machine with limited resources. As you can see on my device, I find it better to use 50% of my CPUs as the max number of workers.
Jest Observability: Adding Personal Observability into Jest tests

What are Jest tests used for:

  • Unit testing – A software testing method used for testing individual units of the source code to determine whether they are a good fit for usage. The main advantage of unit testing over other design methods is that you can use the design document to verify the implementation.
  • Integration testing – A software testing method in which a software application’s different units, modules, or components are tested as a combined entity. 
  • Function testing – A software testing method that validates the software system against the functional requirements/specifications. Functional tests test each function of the software application by providing appropriate input and verifying the output against the applicable requirements.
  • End2End testing – A software testing method used to ensure applications behave as expected and that data flow is maintained for all user tasks and processes.

The reasons for choosing Jest as a testing framework  

  • A progressive mocking system
  • Sandboxing Jest tests (excellent isolation) 
  • Configurable and flexible

A progressive mocking system 

One of the features of Jest that I enjoy most is its advanced mocking system, which means I can create a fake version of a function, a unit, or a module that can stand-in for the real one—enabling me to test only the unit that I need. Testing a single piece of code makes my tests more precise and accurate. It also reduces the time I spend on creating the testing environment. So, for example, if there are modules that I don’t want to test with Jest, I can quickly mock them.

Mocking unwanted behavior (mocking a function) –

/* Code we would like to test */
function LogAndExit(severity, msg, exitCode) {
    logger[severity](msg);
    // That will terminate the program immediately
    process.exit(exitCode);
}
    

So we’ll mock the process.exit using the Jest mocking system and check if the process.exit was called with the argument. Then, for example, you can re-implement the process.exit behavior to do nothing (to avoid the process’s exit).

describe('Test Log & Exit Function', () => {
    it('Should log with severity error and exit', () => {
        const severity = 'error';
        const msg = 'Please log my msg';
        const exitCode = 1;
        
        // Will change process.exit implementation
        jest.spyOn(process, 'exit').mockImplementation((exitCode) => {});
        // Spy on the logger and do not change logger.error implementation
        jest.spyOn(logger, 'error');
        
        // In case that process.exit is not mocked => That will terminate the jest worker process immediately
        LogAndExit(severity, msg, exitCode);
        
        // Test that process.exit & logger.error has been invoked
        expect(process.exit).toBeCalledTimes(1);
        expect(process.exit).toBeCalledWith(exitCode);
        expect(logger.error).toBeCalledTimes(1);
        expect(logger.error).toBeCalledWith(msg);
        
    });
});

Mocking a server’s response – eliminates the need to make the server available. For example, let’s assume we have a function that uses the HTTP module to check if the server returns a status code of 200.

const https = require('https');
 
module.exports = {
    isAlive: async function(url) {
        const statusCode = await new Promise(resolve => https.request(url, res => resolve(res.statusCode)).end());
        return statusCode === 200;
    }
}

Now let’s write a test that does not use the actual HTTPS module and test only the logic of the is Alive function.

describe('Test is alive function', () => {
    it('Should be alive', async () => {
        jest.mock('https', () => ({
            request: async (url, cb) => cb({statusCode: 200})
        }));
        const {isAlive} = require('./isAlive');
        
        await expect(isAlive('https://sprkl.dev')).resolves.toBeTruthy();
    });
});

Sandboxing Jest testing

As you probably know, Sandboxes allow you to create as many spies and stubs as you want without having to track them all and clear them manually. For example, if we use a module with a cache, we can use the module in every test without worrying about what is in the cache. The Jest sandbox system ensures the module is re-evaluated for all the tests from scratch (meaning the cache will be clean). Furthermore, it also allows Jest parallelizing tests over a few processes without worrying about sharing states between tests – which results (usually) in a significant performance boost. 

And, IMO, the primary advantage you get with Jest is the isolation that prevents race conditions between tests – it’s possible but difficult to create a race condition between tests.

Configurable – flexible

Jest’s philosophy is to provide an integrated “zero-configuration” experience. Therefore, if you have special needs for your testing environment, Jest allows you to modify almost every step from the setup phase to the test report phase. For the Sprkl project, we find this modification ability superessential as we could not support Jest without it. 

For example, let’s assume we’d like to start a backend server every time a test is started and stop the server every time the test is completed. This way, we can use the globalSetup and globalTeardown options. 
First, let’s config our jest.config.js file –

module.exports = {
    globalSetup: '<rootDir>/setup.js',
    globalTeardown: '<rootDir>/teardown.js'
};

Now, let’s create our setup & teardown files –

const myServer = require('./backendServer');
 
module.exports = async function(globalConfig, projectConfig) {
    console.log(`Start the backend server for ${globalConfig.rootDir}`);
    myServer.start();
const myServer = require('./backendServer');
 
module.exports = async function(globalConfig, projectConfig) {
    console.log(`Stop the backend server for ${globalConfig.rootDir}`);
    
    myServer.stop();
};

Get more information about Jest’s architecture: https://jestjs.io/docs/architecture 
Get more information about Jest’s configuration options: https://jestjs.io/docs/configuration

So, why did we decide to use Jest for our projects

Jest has a lot of benefits and provides a great testing framework for Nodejs projects. We write some of our projects in TS, so we needed to choose between some testing frameworks. We found Jest the most suitable for our needs because it’s simple yet sophisticated – just how we like it.

Why did we decide to make our Jest tests observable?

Jest is a widespread open-source testing and mockup library in the Node.js ecosystem. Similarly, OpenTelemetry is one of the most robust open-source tools for instrumenting, monitoring, and observability of applications and is emerging as the new standard for observability and application monitoring. However, despite the value derived from fusing these two worlds, there is no simple way to connect these two excellent projects to bring the benefits of open observability into testing processes. But we decided to go for it and integrated these two to add observability to tests automatically, providing you with one trace per test, as we like to say. 

Our goal is to quickly: 

  • Discover which tests were affected by your code changes
  • See traces, variables, and assertions of your affected tests
  • Know when a mockup module is actually called
  • Notice if any specs broke due to code change 

What is Jest Observability: Automated tracing

So this is what the Sprkl team and I did – we implemented a powerful instrumentation library for Jest so you can get a trace per test with automated logging for all your mockups, assertions, and code diffs directly in the IDE. We just feel that devs waste too much time on debugging flaky tests that their code didn’t even touch and that they don’t understand which specs were affected by their code changes – and this must stop.

Sprkl is now available on the VS Code marketplace.

Share

Share on facebook
Share on twitter
Share on linkedin

Enjoy your reading 8 Min Read

Further Reading