Mastering Unit Tests for Developers Using Jest - Part 2

Mastering Unit Tests for Developers Using Jest - Part 2

Introduction

Welcome again to the second part of the Mastering Jest blog. This is the second part of my mastering jest blog. If you haven’t gone through the first part then here’s the link. I am really excited about this blog because we will get to learn a lot in this blog—topics such as setup, Mock functions, Order or execution, and some best practices.

Setup and Teardown

  • beforeEach: Runs before each Test

  • afterEach: Runs after each Test

  • beforeAll: Runs only once at start

  • afterAll: Runs only once at the end

If the setup process is resource-intensive or time-consuming, consider using beforeAll instead of beforeEach. beforeAll sets up the environment once for the entire test suite, reducing overhead for each individual test case.

Best practice for Setup and Teardown

In Jest, beforeAll and afterAll are global setup and teardown functions, respectively. They allow you to perform setup tasks before running any test cases (for beforeAll) and cleanup tasks after running all test cases (for afterAll). These functions are particularly useful when you need to set up common resources or configurations for multiple tests or when you want to clean up any changes made during testing.

Let's assume you have a simple application that fetches data from an API, and you want to test the functionality of the API client. In this example, we'll use beforeAll to set up the API client and afterAll to clean up any resources after all tests are executed.

const ApiClient = require('../apiClient');

let apiClient;

beforeAll(() => {
  // This runs once before all test cases
  apiClient = new ApiClient();
});

afterAll(() => {
  // This runs once after all test cases are done
  apiClient = null; // Clean up the API client instance
});

test('should fetch data from the API', async () => {
  // Arrange
  const url = '<https://jsonplaceholder.typicode.com/todos/1>';

  // Act
  const data = await apiClient.fetchData(url);

  // Assert
  expect(data.userId).toBe(1);
  expect(data.id).toBe(1);
  expect(data.title).toBeTruthy();
  expect(data.completed).toBeDefined();
});

test('should handle API errors', async () => {
  // Arrange
  const invalidUrl = '<https://jsonplaceholder.typicode.com/invalid>';

  // Act and Assert
  await expect(apiClient.fetchData(invalidUrl)).rejects.toThrow();
});

In this example, we use beforeAll to instantiate the ApiClient once before running any test cases. This ensures that the API client is set up and ready to be used by all test cases.

We also use afterAll to clean up after all the tests are done. In this case, we set the apiClient variable to null to release any resources held by the API client instance.

Scoping

describe: Use describe block to separate your test cases

The hooks declared inside a describe block apply only to the tests within that describe block

Order of Execution

  • Jest executes all describe handlers in a test file before it executes any of the actual tests

  • This is another reason to do setup and teardown inside before* and after* handlers rather than inside the describe blocks

  • Once the describe blocks are complete, by default Jest runs all the tests serially in the order they were encountered in the collection phase

describe('describe outer', () => {
  console.log('describe outer-a');

  describe('describe inner 1', () => {
    console.log('describe inner 1');

    test('test 1', () => console.log('test 1'));
  });

  console.log('describe outer-b');

  test('test 2', () => console.log('test 2'));

  describe('describe inner 2', () => {
    console.log('describe inner 2');

    test('test 3', () => console.log('test 3'));
  });

  console.log('describe outer-c');
});

// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test 1
// test 2
// test 3

Mock Functions

  • Mock functions in Jest are a powerful feature that allows you to create and control "fake" implementations of functions, methods, or modules during testing

  • You can create a mock function using jest.fn() or jest.mock():

// Standalone mock function
const mockFunction = jest.fn();
  • You can configure the behavior of mock functions using Jest matchers such as mockReturnValue, mockResolvedValue, mockRejectedValue, and more.
const mockFunction = jest.fn();
mockFunction.mockReturnValue(42); // Always return 42 when called
// Return a resolved promise with the specified data
mockFunction.mockResolvedValue({ data: 'mocked data' });

Asserting Mock Function Calls:

You can use matchers like toHaveBeenCalled, toHaveBeenCalledTimes, toHaveBeenCalledWith, and others to verify if and how the mock function was called during the test.

const mockFunction = jest.fn();

// Call the function
mockFunction('param');

// Assertions
expect(mockFunction).toHaveBeenCalled();
expect(mockFunction).toHaveBeenCalledTimes(1);
expect(mockFunction).toHaveBeenCalledWith('param');

Reset Mock Functions

Jest automatically tracks the usage of mock functions during test runs. After each test, the mock functions are reset, clearing any calls, return values, or other configurations.

const mockFunction = jest.fn();
// Call the function
mockFunction('param');
// Reset the mock function
mockFunction.mockReset();

Jest Best Practices

  1. Isolate Tests: Ensure each test case is independent and does not rely on the state of other tests. Isolation avoids interference and provides accurate results.

  2. Descriptive Test Names: Use meaningful and descriptive test names to make it clear what each test is validating.

  3. Arrange-Act-Assert Pattern: Follow the AAA pattern to structure your tests: Arrange the test setup, Act on the code being tested, and Assert the expected outcome.

  4. Use Matchers: Utilize Jest's matchers for expressive assertions, improving test readability.

  5. Mock External Dependencies: Mock external services or complex functions to isolate code during testing.

  6. Snapshot Testing for UI Components: Use snapshot testing to ensure the output of UI components remains consistent over time.

  7. Test Coverage: Aim for good test coverage to ensure comprehensive testing of your code.

  8. Use async/await: Handle asynchronous code gracefully using async/await or return promises in tests.

  9. Before and After Hooks: Use beforeEach and afterEach for common setup and teardown tasks.

  10. beforeAll and afterAll: Use beforeAll and afterAll for global setup and cleanup tasks.

  11. Test Organization: Group related tests using describe blocks to improve test organization.

  12. Clear Side Effects: Clean up any side effects or global state changes introduced during tests.

  13. Continuous Integration: Set up automated testing with Jest in your CI/CD pipeline.

  14. Keep Tests Fast: Write efficient tests to maintain fast test execution times.

  15. Update Snapshots: Regularly update snapshots to reflect intentional changes in the output of components.

By following these best practices, you can create well-structured, reliable, and maintainable test suites, enabling you to confidently ship high-quality code with Jest.

We have arrived at the end of this blog, I have covered all the important topics for Jest testing framework. I tried to cover almost everything but still, there is lot to learn. I will keep coming with new blogs so stay tuned. Thank you for reading this blog.

Did you find this article valuable?

Support Ashish Jaiswar by becoming a sponsor. Any amount is appreciated!