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​