Skip to content

Instantly share code, notes, and snippets.

@juliosguz
Last active August 5, 2025 16:08
Show Gist options
  • Select an option

  • Save juliosguz/ad7bd217082091cf0a45afb19470828a to your computer and use it in GitHub Desktop.

Select an option

Save juliosguz/ad7bd217082091cf0a45afb19470828a to your computer and use it in GitHub Desktop.
Jest Testing Best Practices

Jest Testing Best Practices

File Structure

  1. Imports Organization

    // External dependencies first
    import { v4 as uuidv4 } from 'uuid';
    
    // Internal dependencies
    import { channelInviteService } from '../../../../controllers/channels/invite-users.controller';
  2. Type Definitions

    interface TestData {
      // Define your test data types
    }
    
    interface ServiceResponse {
      // Define your service response types
    }
  3. Mock Declarations

    // Group all mocks at the top
    jest.mock('../../../../util/general.util', () => ({
      getUserIdAndValidate: jest.fn(),
    }));

Test Structure

A good structure makes tests readable, robust, and easy to maintain. For this, we rely on a three-step philosophy that describes the behavior being tested.

  1. The AAA Pattern (Arrange/Act/Assert) & its Connection to Given-When-Then

The gold standard for structuring tests is the Arrange-Act-Assert (AAA) pattern. This pattern is the practical, in-code implementation of the Given-When-Then concept from Behavior-Driven Development (BDD), which aims to bridge the gap between business requirements and technical implementation.

  • Arrange (Given): Set up the context and initial conditions. This includes creating test data, configuring mocks, and any other preparation required for the scenario.
  • Act (When): Execute the action or piece of code you want to test. Ideally, this is a single call to a function or method.
  • Assert (Then): Verify that the outcome of the action is as expected. This includes checking return values, side effects, mock calls, or the final state of the system.

The most effective way to document this connection is by embedding the business-facing scenario directly into the test comments. This makes the test's purpose immediately clear to anyone, technical or not.

Here is a complete example showing how a business requirement (written in Gherkin) is directly translated into a Jest test:

/*
 * Feature: Channel Creation for Logged-in Users
 *
 * Scenario: An authenticated user creates a new public channel with a valid name
 * Given an authenticated user is in the system
 * When the user requests to create a new public channel named "Marketing Channel"
 * Then the system should create the channel with the name "Marketing Channel"
 * And the system should respond with a "success" status and the new channel's data
 */
it('should allow a logged-in user to create a new public channel with a valid name', async () => {
  // --- Arrange (Given) ---
  // "an authenticated user is in the system"
  const mockAuthenticatedUserId = 'user-auth-123';
  
  // "the user requests to create a new public channel..."
  const requestData = {
    channelName: 'Marketing Channel',
    isPublic: true,
  };
  
  // "...and the new channel's data" (this is what we expect the service to return)
  const expectedChannel = {
    id: 'channel-xyz-789',
    name: requestData.channelName,
    ownerId: mockAuthenticatedUserId,
    public: true,
  };

  // Configure the mock to simulate a successful database creation
  channelCreationService.mockResolvedValue(expectedChannel);

  // --- Act (When) ---
  // "When the user requests to create a new public channel..."
  const response = await handleChannelCreation(requestData, mockAuthenticatedUserId);

  // --- Assert (Then) ---
  // "the system should create the channel..." (we verify the correct service was called)
  expect(channelCreationService).toHaveBeenCalledWith({
    name: requestData.channelName,
    isPublic: requestData.isPublic,
    ownerId: mockAuthenticatedUserId,
  });
  
  // "...and respond with a 'success' status and the channel's data"
  expect(response).toEqual({
    status: 'success',
    data: expectedChannel,
  });
});
  1. Reusable Test Data

    const mockData = {
      id: uuidv4(),
      // other mock data
    };
  2. Cleanup

    beforeEach(() => {
      jest.clearAllMocks();
    });

Assertions

  1. Use toMatchObject for Partial Matches

    expect(result).toMatchObject({
      status: CREATED,
      data: expect.objectContaining({
        id: expect.any(String)
      })
    });
  2. Function Call Verification

    expect(mockFunction).toHaveBeenCalledWith(
      expect.objectContaining({
        id: mockId
      })
    );
  3. Array Assertions

    expect(result.array).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          id: mockId
        })
      ])
    );

Error Handling

  1. Error Cases

    it('should handle errors', async () => {
      // Arrange
      const mockError = new Error('Test error');
      mockFunction.mockRejectedValue(mockError);
      
      // Act & Assert
      await expect(testFunction()).rejects.toThrow('Test error');
    });
  2. Validation Errors

    it('should handle validation errors', async () => {
      const invalidData = { /* invalid data */ };
      await expect(testFunction(invalidData))
        .rejects
        .toThrow('Validation error');
    });

Best Practices

  1. Use descriptive test names: The it block should clearly state what behavior is being tested.
  2. Structure tests with Given/When/Then (AAA): Always follow the Arrange-Act-Assert pattern. This structure makes tests predictable and easy to read by clearly separating setup, execution, and verification, as detailed in the Test Structure section.
  3. Keep tests focused and atomic: Each test should verify a single piece of functionality. Avoid testing multiple, unrelated behaviors in one it block.
  4. Mock only what's necessary: Isolate the unit under test by mocking its external dependencies (services, APIs, etc.), but avoid mocking modules that are part of the functionality being tested.
  5. Use TypeScript for better type safety: Leverage TypeScript to catch errors early and ensure data structures are consistent.
  6. Document complex test scenarios: For tests with intricate setup or non-obvious logic, use comments to explain the "why" behind your implementation. Embedding the Gherkin scenario is a great way to do this.
  7. Use meaningful variable names: Names like mockUser or expectedResult are clearer than obj1 or temp.
  8. Keep test data close to the test: Define test-specific data inside the it block or right before it. For shared data, use a well-named constant.
  9. Clean up after each test: Use beforeEach or afterEach to clear mocks, spies, and reset any state to prevent tests from interfering with each other.
  10. Test both success and error cases: A robust test suite validates both the happy path and how the code handles expected errors (e.g., validation errors, exceptions).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment