Jest test lifecycle and test environment

Yiqun Rong
8 min readNov 12, 2023

--

If you are a Javascript or NodeJs developer, you must hear of Jest as it can be used in unit test and integration tests for both frontend and backend. It is one of the most popular testing framework in the JS world. In this story, I am going to take you a deep dive into the Jest test lifecycle and jest environment.

Jest test lifecycle

There are several stages in Jest test lifecycle.

1. Global Setup (Optional). Before any tests are run, Jest can perform a global setup if configured. This step is used for heavy, one-time setup tasks that don’t need to be repeated before each test file is executed.

2. Test File Execution. Jest runs each test file in a separate Node process. This helps isolate tests, ensuring clean and independent environments for each test file. Jest can run multiple test files in parallel, depending on the number of available CPU cores.

3. Test Environment Setup. For each test file, Jest sets up the test environment as specified in the Jest configuration (e.g., jsdom for browser-like environment, node for Node.js environment).

4. Test Suite Execution. Jest executes tests as they are encountered in the test file. Tests are usually organized in describe blocks, which are essentially test suites.

​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​a. The beforeAll Hooks. If a test suite (describe block) contains beforeAll hooks, these run before any of the tests in that suite are executed. They are used for setting up conditions common to all tests in the suite.

​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​b. The Test Execution. Each test (defined with it or test) is executed in order.

​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​i. ThebeforeEach Hooks. For each test, beforeEach hooks run immediately before the test. They are used for setting up conditions that should be applied to every test individually.

​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​ii. The Test Logic. The actual test logic is executed.

​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​ ​iii. The afterEach Hooks. After the test logic, afterEach hooks run. These are used for cleanup activities that should occur after each test.

​ ​​​ ​​ ​​​ ​ ​​ ​ ​​​ ​ ​​ ​ ​​​c. The afterAll Hooks. Once all tests in a describe block have been executed, afterAll hooks run. These are suitable for cleanup activities that apply to the entire test suite.

5. Test Environment Teardown. After all tests in a file have been executed, the test environment is torn down.

6. Global Teardown (Optional) Finally. after all test files have been executed, a global teardown can be performed. This step is for shutting down any global resources initialised before the tests started.

Jest supports asynchronous code in all these hooks and tests. You can use async/await or return Promises to ensure Jest waits for asynchronous operations to complete. Jest runs test files in parallel to speed up the test suite. However, tests within a file run sequentially.Each test file is run in its own isolated environment, which helps to prevent tests from affecting each other and makes them more reliable.

[Start]
|
|---[Global Setup]---|
| |
|--> [Test File 1] --|---> [Test Environment Setup]
| | | |
| |--> [describe block] |
| | |
| |---> [beforeAll]|
| | |
| |--> [Test 1] |
| | | |
| | |--[beforeEach]
| | |--[Test Execution]
| | |--[afterEach]
| | |
| |--> [Test 2] |
| | | |
| | |--[beforeEach]
| | |--[Test Execution]
| | |--[afterEach]
| | |
| |---> [afterAll] |
| | |
| |--> [more tests or describe blocks]
| |
|--> [Test Environment Teardown]
|
|--> [Test File 2] --| [similar structure to Test File 1]
|--> [Test File 3] --| [similar structure to Test File 1]
|
|---[Global Teardown]---|
|
[End]

What is the corresponding events emitted from different test stages?

  1. setup: Triggered when setting up the test environment for a test file.
  2. teardown: Triggered when tearing down the test environment after a test file has completed.
  3. test_start: Emitted when an individual test starts.
  4. test_done: Emitted when an individual test finishes.
  5. hook_start: Emitted when a lifecycle hook (like beforeEach or afterEach) starts.
  6. hook_success: Emitted when a lifecycle hook completes successfully.
  7. hook_failure: Emitted when a lifecycle hook fails.
  8. suite_start: Emitted when a describe block starts.
  9. suite_done: Emitted when a describe block finishes.
  10. error: Emitted if there is an error during the test run that is not associated with a specific test or hook failure.
[Start]
|
|---[Global Setup]---| (No direct handleTestEvent)
| |
|--> [Test File 1] --|---> [Test Environment Setup] (No direct handleTestEvent)
| | | |
| |--> [describe block] |--- [suite_start] event
| | |
| |---> [beforeAll] |--- [hook_start] event (for beforeAll)
| | |
| |--> [Test 1] |
| | | |
| | |--[beforeEach]--- [hook_start] event (for beforeEach)
| | |--[Test Execution]--- [test_start] event
| | | [test_fn_start] event (optional)
| | | [test_fn_success/failure] event (optional)
| | |--[afterEach]--- [hook_end] event (for afterEach)
| | |
| |--> [Test 2] |--- [test_start] event
| | | | (and similar for beforeEach, afterEach)
| | |--[beforeEach]
| | |--[Test Execution]
| | |--[afterEach]
| | |
| |---> [afterAll] |--- [hook_end] event (for afterAll)
| | |
| |--> [more tests or describe blocks]
| |
| |--- [suite_done] event
| |
|--> [Test Environment Teardown] (No direct handleTestEvent)
|
|--> [Test File 2] --| [similar structure and events to Test File 1]
|--> [Test File 3] --| [similar structure and events to Test File 1]
|
|---[Global Teardown]---| (No direct handleTestEvent)
|
[End]

Test Environment

In Jest, the “Test Environment” specifies the environment in which your tests are executed. It defines the global variables that are available and how certain operations, such as timers and DOM manipulation, are handled. Jest provides different environments to cater to different needs, typically for Node.js or browser-like environments.

Default Environments in Jest

  1. jsdom: This is Jest’s default environment. It simulates a browser environment using jsdom. This environment provides a mock DOM, allowing you to test your JavaScript code as if it were running in a web browser. It’s particularly useful for testing front-end code that interacts with web pages or manipulates the DOM.
  2. node: This environment is for testing Node.js server-side applications. It does not mock a DOM and is best suited for testing code that runs in a Node.js context, like server-side scripts, APIs, or command-line tools.

Custom Test Environments

We can also create custom test environments in Jest. This is useful if you have specific requirements that are not met by the built-in environments. For example, you might want to create a test environment that includes certain polyfills or mocks specific global functions in a certain way.

A custom environment is a JavaScript class that extends one of the built-in environments and overrides its methods. Here’s a basic structure of a custom environment:

const NodeEnvironment = require('jest-environment-node');

class MyCustomEnvironment extends NodeEnvironment {
constructor(config) {
super(config);
// Custom initialization
}

async setup() {
await super.setup();
// Setup code (executed before each test file)
}

async teardown() {
// Teardown code (executed after each test file)
await super.teardown();
}

runScript(script) {
return super.runScript(script);
}
}

module.exports = MyCustomEnvironment;

Practical examples for Custom Test Environment

Custom test environments in Jest are extremely useful in various scenarios, especially when you need a specific setup that goes beyond what the default environment offers. Here are some practical use cases:

1. Integrating with a Database: If your tests need to interact with a database, you might want to set up a test database connection in the custom environment. This setup can include creating a temporary database, seeding it with data, and tearing it down after tests are completed.

// Custom environment setup for a database
class CustomTestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
this.global.dbConnection = await database.connect();
}

async teardown() {
await database.disconnect();
await super.teardown();
}
}

2. Mocking Global Objects: For applications that rely on global objects like window or document in a browser environment, or specific Node.js globals, you can set up these objects in your custom environment. This is particularly useful for server-side rendering tests in React or other frameworks.

// Mocking global objects like window or document
class CustomTestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
this.global.window = { ... };
this.global.document = { ... };
}
}

3. Setting Up a Mock Server: If your application makes network requests, you might want to intercept these requests and mock responses during testing. A custom environment allows you to start a mock server before tests run and shut it down afterwards.

// Setting up a mock server
class CustomTestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
this.global.mockServer = startMockServer();
}

async teardown() {
this.global.mockServer.close();
await super.teardown();
}
}

4. Configuring a Custom DOM Environment: For testing client-side JavaScript that interacts with the DOM, you might need a custom DOM setup, such as a virtual DOM or a specific DOM structure. This can be set up in the custom environment.

// Custom DOM setup
const { JSDOM } = require('jsdom');

class CustomTestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
this.global.window = jsdom.window;
this.global.document = jsdom.window.document;
}
}

5. Environment-Specific Configurations: When tests need different environment configurations, like different API endpoints, feature flags, or third-party service mocks, a custom test environment can be used to switch between these configurations based on the testing context.

// Environment-specific configurations
class CustomTestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
this.global.apiEndpoint = 'https://mock-api-endpoint';
}
}

6. Integrating with Third-party Services: If your tests depend on third-party services or APIs, you can set up mocks or stubs for these services in the custom environment to ensure your tests are not making actual API calls.

// Mocking third-party services
class CustomTestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
this.global.thirdPartyService = mockThirdPartyService();
}
}

7. Performance Monitoring: In a custom environment, you can also integrate performance monitoring tools to check the performance of your code during the test runs.

// Performance monitoring setup
class CustomTestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
this.global.performanceMonitor = startPerformanceMonitoring();
}

async teardown() {
this.global.performanceMonitor.stop();
await super.teardown();
}
}

8. Handling Complex Asynchronous Setup: When your tests require complex asynchronous setup processes, like waiting for certain events or conditions before starting tests, a custom environment can be a good place to handle these setups.

// Asynchronous setup
class CustomTestEnvironment extends NodeEnvironment {
async setup() {
await super.setup();
await waitForSomeAsyncCondition();
}
}

Configuring the Test Environment

You specify the test environment in Jest’s configuration. For example, to use the Node environment, you would update your Jest configuration like this:

{
"testEnvironment": "node"
}

if use custom environment:

{
"testEnvironment": "<path-to-your-custom-environment>"
}

Conclusion

In this story, we discussed some concepts of Jest lifecycle and Jest test environment. Understanding this lifecycle allows you to structure your tests effectively, placing setup and teardown code in the appropriate hooks, and ensuring that each test is isolated and independent. The custom test environment also enables higher flexibility and higher of degree of freedom on test setup.

--

--