Skip to main content

Jest Guide

Overview

Jest is the primary testing framework for unit and integration tests in humuus. It provides a robust environment for testing both frontend React components and backend TypeScript functions in isolation.

When to Use Jest

  • Unit Tests: Testing individual functions, classes, or React components
  • Integration Tests: Testing the interaction between multiple components or modules
  • Mock-dependent Tests: When you need to simulate external dependencies like API calls or database operations

Setup and Configuration

Jest is already configured in the humuus project. Tests are automatically run through GitHub Actions when creating pull requests to the main or dev branches.

Writing Unit Tests

Basic Test Structure

import { jest } from '@jest/globals';

describe('ComponentName or FunctionName', () => {
beforeEach(() => {
// Reset mocks and setup before each test
jest.clearAllMocks();
});

it('describes what the test does', () => {
// Arrange: Setup test data
// Act: Execute the function or render the component
// Assert: Check the results
expect(result).toBe(expectedValue);
});

afterEach(() => {
// Cleanup after each test
jest.restoreAllMocks();
});
});

Testing React Components

/**
* @jest-environment jsdom
*/
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import ComponentName from '@/components/ComponentName';

describe('ComponentName', () => {
it('renders component with correct text', () => {
render(<ComponentName />);

expect(screen.getByText('Expected Text')).toBeInTheDocument();
});

it('renders button and handles click', () => {
const handleClick = jest.fn();
render(<ComponentName onClick={handleClick} />);

const button = screen.getByRole('button', { name: 'Click Me' });
expect(button).toBeInTheDocument();
});

it('displays data from props', async () => {
const testData = { title: 'Test Title' };
render(<ComponentName data={testData} />);

await waitFor(() => {
expect(screen.getByText('Test Title')).toBeInTheDocument();
});
});
});

Testing Backend Functions

import { jest } from '@jest/globals';

describe('RPC Function: createMatch', () => {
const mockNk = {
matchCreate: jest.fn().mockReturnValue('match-123'),
accountGetId: jest.fn().mockReturnValue({
user: { displayName: 'TestUser', avatarUrl: '' }
}),
};

const mockLogger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
};

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

it('creates a match and returns match ID', () => {
const result = createMatch(mockNk, mockLogger, 'user-1', {});

expect(mockNk.matchCreate).toHaveBeenCalledTimes(1);
expect(result).toBe('match-123');
expect(mockLogger.info).toHaveBeenCalled();
});

it('handles errors correctly', () => {
mockNk.matchCreate.mockImplementation(() => {
throw new Error('Creation failed');
});

expect(() => createMatch(mockNk, mockLogger, 'user-1', {})).toThrow();
expect(mockLogger.error).toHaveBeenCalled();
});
});

Mocking Dependencies

Mocking Modules

// Mock external libraries
jest.mock('react-markdown', () => {
return (props: { children: string }) => <div>{props.children}</div>;
});

jest.mock('@/services/workshopService', () => ({
WorkshopService: jest.fn().mockImplementation(() => ({
appendNodeToWorkshop: jest.fn().mockResolvedValue({
success: true,
newNodeIndex: 5,
}),
updateNodeInWorkshop: jest.fn().mockResolvedValue({
success: true,
}),
})),
}));

Creating Mock Functions

const mockFunction = jest.fn();

// Mock implementation
mockFunction.mockImplementation((param) => {
return `processed-${param}`;
});

// Mock return value
mockFunction.mockReturnValue('static-value');

// Mock resolved promise
mockFunction.mockResolvedValue({ success: true });

// Mock rejected promise
mockFunction.mockRejectedValue(new Error('Failed'));

Verifying Mock Calls

it('calls function with correct parameters', () => {
const mockSendData = jest.fn();

component.sendData('test-data');

expect(mockSendData).toHaveBeenCalledTimes(1);
expect(mockSendData).toHaveBeenCalledWith('test-data');

// Check all calls
expect(mockSendData.mock.calls[0][0]).toBe('test-data');
});

Testing Asynchronous Code

it('handles async operations', async () => {
const result = await fetchData();

expect(result).toBeDefined();
expect(result.data).toBe('expected-data');
});

it('waits for element to appear', async () => {
render(<AsyncComponent />);

await waitFor(() => {
expect(screen.getByText('Loaded Data')).toBeInTheDocument();
});
});

Common Assertions

// Equality
expect(value).toBe(expectedValue);
expect(object).toEqual(expectedObject);

// Truthiness
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();

// Numbers
expect(number).toBeGreaterThan(5);
expect(number).toBeLessThan(10);
expect(number).toBeCloseTo(5.5);

// Strings
expect(string).toMatch(/pattern/);
expect(string).toContain('substring');

// Arrays
expect(array).toContain(item);
expect(array).toHaveLength(3);

// DOM Elements (with @testing-library/jest-dom)
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toHaveTextContent('text');

Best Practices

1. Test Isolation

  • Each test should be independent
  • Use beforeEach and afterEach for setup and cleanup
  • Always clear mocks between tests
beforeEach(() => {
jest.clearAllMocks();
});

afterEach(() => {
jest.restoreAllMocks();
});

2. Descriptive Test Names

// Good
it('creates match and returns match ID when valid user provided', () => {});

// Bad
it('works', () => {});

3. Arrange-Act-Assert Pattern

it('calculates total score correctly', () => {
// Arrange
const scores = [10, 20, 30];

// Act
const total = calculateTotal(scores);

// Assert
expect(total).toBe(60);
});

4. Test Error Cases

it('throws error when invalid input provided', () => {
expect(() => processData(null)).toThrow('Invalid input');
});

it('handles network errors gracefully', async () => {
mockApi.mockRejectedValue(new Error('Network error'));

const result = await fetchData();

expect(result.error).toBe('Network error');
});

5. Keep Tests Focused

  • Test one behavior at a time
  • Avoid testing implementation details

Running Tests

# Run all tests
npm test

# Run tests in watch mode
npm test -- --watch

# Run specific test file
npm test -- ComponentName.test.tsx

# Run with coverage
npm test -- --coverage

Integration with CI/CD

Tests automatically run through GitHub Actions on pull requests to main and dev branches. Ensure all tests pass before merging.

Common Pitfalls

  1. Not cleaning up mocks: Always use jest.clearAllMocks() in beforeEach
  2. Testing implementation details: Focus on behavior, not internal structure
  3. Async tests without await: Always await async operations
  4. Forgetting @jest-environment jsdom: Required for React component tests
  5. Not mocking external dependencies: Can cause tests to be slow or flaky

Resources