DEV Community

Cover image for Jest: A Delightful JavaScript Testing Framework
Tyler Meyer
Tyler Meyer

Posted on

Jest: A Delightful JavaScript Testing Framework

What is Jest?

To quote the Jest Core Team, "Jest is a JavaScript testing framework designed to ensure correctness of any JavaScript codebase." [1] Regarding software development, testing code provides a safety net that can prevent bugs and improve the code written. Setting up team workflows using Test Driven Development and Continuous Integration requires a reliable testing framework. Throughout this article, I will go over the strengths of Jest, the downsides, tips for setting up a test suite, and how to write tests.

The Strengths of Jest

I will compare Jest to another testing framework called Mocha for these strengths. Depending on the use case, developers will find either of these testing frameworks practical, depending on their needs. While critiquing Mocha, I will also point out its strength compared to Jest.

Simplicity

Jest is easier to set up than Mocha. Mocha usually requires additional external tools to create a complete testing suite, while Jest works independently without additional libraries or tools. [2]

Snapshot Testing

Snapshot testing is a Jest tool that ensures the UI does not change unexpectedly. Snapshot testing works by rendering a UI component, taking a snapshot, and then comparing the snapshot to a reference of the expected snapshot alongside the test. [3]

React & TypeScript Integration

Meta initially developed Jest to address their internal testing needs for their chat feature in 2011, which means it was primarily designed to work with React applications. Jest also provides support for TypeScript projects that want to ensure type safety.

Community Support

Jest provides extensive documentation, and with a large community, you will find help with troubleshooting issues and best practices to follow.

The Potential Downsides of Jest

Not as Flexible

Compared to frameworks like Mocha, Jest is less flexible for customized testing scenarios. Since Mocha is designed to integrate with external tools, you can write tests for your specific needs. Creating custom test suites with Jest will require more work, which may cause slower performance.

Performance Concerns

While Jest is generally fast, extensive test suites might sometimes experience performance degradation. To counter this, you can break more extensive tests into smaller, more focused units.

Tips for Setting Up Jest Test Suites

Configure Jest

Jest provides an extensive list of options for configuring the testing framework for your needs, depending on the type of testing you're conducting. I'll provide you with some sample code for testing server endpoints.

// jest.config.js

module.exports = {
  preset: 'ts-jest',
  globalSetup: './setup.ts',
  globalTeardown: './teardown.ts',
};
Enter fullscreen mode Exit fullscreen mode

I prefer to use TypeScript to provide type safety for my code. By installing ts-jest and setting up a preset for it, Jest will recognize when I'm using TypeScript and transpile my code for testing. globalSetup and globalTeardown provide a way to perform actions before and after all test suites have run. In the following two code snippets, I'll set up a test server and then close the server in the teardown.

// setup.ts

import express, { Application } from 'express';
import http from 'http';
import apiRouter from '../../server/api';  

declare global {
  var PORT: number;
  var testServer: http.Server;
}

export default async function () {
  function promiseListen(exApp: Application): Promise<http.Server> {
    return new Promise<http.Server>((resolve, reject) => {
      const server = exApp.listen(PORT, () => {
        console.log(`Server listening on port ${PORT}.`);
        resolve(server);
      });

      server.on('error', (error: Error) => {
        reject(error);
      });
    });
  }

  const PORT = 8000;

  try {
    const app = express();
    app.use(express.json());
    app.use('/api', apiRouter); // Beginning of endpoints to be tested
    let testServer: http.Server = await promiseListen(app);

    globalThis.testServer = testServer;
    globalThis.PORT = PORT;
  } catch (error: unknown) {
    console.error('Failed to start server before testing:', error);
  }
}
Enter fullscreen mode Exit fullscreen mode
// teardown.ts

import http from 'http';

declare global {
  var testServer: http.Server;
};

export default async function () {
  function promiseServerClose(server: http.Server): Promise<void> {
    return new Promise<void>((resolve) => {
      server.close(() => {
        resolve();
      })
    });
  }

  try {
    await promiseServerClose(globalThis.testServer);
  } catch (error: unknown) {
    console.error('Failed to close server after testing:', error);
  }
}
Enter fullscreen mode Exit fullscreen mode

These two code snippets run before and after all test suites. I use the globalThis object to pass the server created in setup.ts to teardown.ts to close the server once our tests are finished. I also pass the PORT number to globalThis to use in my test suites when they request the test server.

If you need a different configuration for another set of tests, you can move your previously created config, setup, and teardown into its own folder along with the test files. In the root directory, you can use one jest config to list the projects to be tested with the jest command as your test script.

// jest.config.js

module.exports = {
  projects: [
    './tests/server' // The new location of the previous code snippets
  ],
};
Enter fullscreen mode Exit fullscreen mode

Jest will go to each project and run the tests using the correct configuration.

Writing Tests With Jest

I will briefly go over two code snippets to show you how to write a test using the Jest testing framework. The first example will be testing for a function. The second example will make use of the test server we created earlier.

// myFunc.ts

export default function myFunc(str: string): string[] {
  return str.split('');
}

// myFunc.test.ts

import myFunc from './myFunc';

describe('Testing myFunc', () => {
  test('myFunc returns something', () => {
    expect(myFunc('abc')).toBeDefined(); // Something is returned
  });

  test('myFunc returns an Array', () => {
    expect(Array.isArray(myFunc('abc'))).toBe(true); // Must be true
  });

  test('myFunc creates an array of correct length', () => {
    expect(myFunc('abc')).toHaveLength(3); // Array's length is three
  });

  test('myFunc creates the correct array', () => {
    expect(myFunc('abc')).toEqual(['a', 'b', 'c']);
    // Recursively check all items in the returned array for equality
  });
});
Enter fullscreen mode Exit fullscreen mode

The outer function describe defines the current test suite. For the test suite to pass, it needs at least one test, and every test must pass. The inner functions test are titled to provide information to the developer when a test doesn't pass. There are several ways to test using expect, and it comes with many "matchers" that let you validate your code.

// tests/server/api.test.ts

import axios from 'axios';
import http from 'http';

declare global {
  var PORT: number;
  var testServer: http.Server;
}

const PORT = globalThis.PORT; // To keep the port consistent across tests

describe('Testing /api Endpoints', () => {
  test ('GET /api/users responds with 200 status code', async () => {
    const res = await axios.get(`http://localhost:${PORT}/api/users`);
    expect(res.status).toBe(200);
  });

  test ('GET /api/users responds with users data', async () => {
    const { data: users } = await axios.get(`http://localhost:${PORT}/api/user`);
    expect(users).toBeDefined(); // Something is sent back
    expect(Array.isArray(users)).toBe(true); // An array is sent back
    expect(users[0]).toBeTruthy(); // The array contains a user
    expect(users[0]).toHaveProperty('username'); // User has 'username'
    expect(users[0]).not.toHaveProperty('SUPER_SECRET_PASSWORD');
    // Does not send back their 'SUPER_SECRET_PASSWORD'
  });
});
Enter fullscreen mode Exit fullscreen mode

Using async/await, we can make asynchronous calls and test the response we receive. In the second test, I used several matchers for one test, which is allowed if you want to check several aspects of one action. I also used the not key for the matchers, which works as you'd expect by negating the expectation.

Conclusion

The technology used in an application is a major consideration in software development. There isn't one perfect testing framework, but Jest is great if you're new to test-driven development and Continuous Integration. With its simplicity, large community, and relatively fast execution, you will be pleased with what you can accomplish with Jest.

Happy Coding!
Tyler Meyer

Sources

[1] https://jestjs.io/

[2] https://saucelabs.com/resources/blog/jest-vs-mocha

[3] https://jestjs.io/docs/snapshot-testing

[4] https://apidog.com/blog/jest-vs-jasmine/

[5] https://jestjs.io/docs/configuration

[6] https://jestjs.io/docs/expect

Top comments (0)