Skip to main content

Overview

Skiff uses a comprehensive testing stack built on Jest and React Testing Library to ensure code quality and reliability across all applications.
The project uses Jest 28.1.1 with SWC for fast compilation, replacing the traditional Babel setup.

Testing Stack

Core Tools

Jest

Version: 28.1.1Test runner and assertion library with built-in mocking and coverage reporting.

SWC

Version: 0.2.15 (@swc/jest)Fast TypeScript/JavaScript compiler that replaces Babel for faster test execution.

React Testing Library

Version: 12.1.2Testing utilities that encourage best practices by testing components as users interact with them.

jest-dom

Version: 5.16.2Custom matchers for asserting on DOM nodes (e.g., toBeInTheDocument()).

Additional Libraries

  • @testing-library/react-hooks (7.0.2): Test React hooks in isolation
  • @testing-library/user-event (14.1.1): Simulate user interactions
  • @testing-library/dom (8.13.0): DOM testing utilities
  • jest-environment-jsdom (28.1.1): Browser-like environment for tests
  • jest-canvas-mock (2.4.0): Mock canvas API for skemail-web
  • fake-indexeddb (4.0.0): Mock IndexedDB for calendar-web tests
  • msw (0.48.0): Mock Service Worker for API mocking (calendar-web)

Running Tests

Application-Level Tests

Each application has its own test suite:
# Run all tests
yarn workspace skemail-web test

# Watch mode
yarn workspace skemail-web test-watch

# With coverage
yarn workspace skemail-web test --coverage
Test configuration:
  • Max workers: 2
  • Heap monitoring: Enabled
  • Memory limit: 4096 MB

Type Checking

Run TypeScript compiler without emitting files:
# Check types in skemail-web
yarn workspace skemail-web ts

# Check types in calendar-web
yarn workspace calendar-web ts
The test script runs type checking before executing tests to catch type errors early.

Test Organization

Test File Locations

Tests are organized in two ways:
  1. Dedicated test directory: skemail-web/tests/
    skemail-web/
    ├── tests/
    │   ├── ComposePanel.test.tsx
    │   ├── ThreadBlock.test.tsx
    │   ├── userUtils.test.ts
    │   └── ...
    
  2. Co-located with source: src/**/*.test.ts
    skemail-web/
    ├── src/
    │   ├── utils/
    │   │   ├── emailUtils.ts
    │   │   └── emailUtils.test.ts
    │   └── components/
    │       └── Settings/
    │           └── Filters/
    │               ├── Filters.utils.ts
    │               └── Filters.utils.test.ts
    

Naming Conventions

  • Component tests: ComponentName.test.tsx
  • Utility tests: utilityName.test.ts
  • Hook tests: useHookName.test.tsx

Writing Tests

Basic Test Structure

Here’s an example from the codebase:
import { formatEmailAddress } from 'skiff-front-utils';
import { getMailDomain, isSkiffAddress } from 'skiff-utils';

describe('userUtils', () => {
  describe('formatEmailAddress', () => {
    it('returns unaltered email address if already valid', () => {
      const testEmail = '[email protected]';
      const actual = formatEmailAddress(testEmail);
      expect(actual).toBe(testEmail);
    });

    it('appends email domain to wallet address', () => {
      const testEthAddress = '0x1111000000000000000000000000000000001111';
      const abbreviated = formatEmailAddress(testEthAddress, true);
      expect(abbreviated).toBe('0x111...1111' + '@' + getMailDomain());

      const fullLength = formatEmailAddress(testEthAddress, false);
      expect(fullLength).toBe(testEthAddress + '@' + getMailDomain());
    });
  });

  describe('isSkiffAddress', () => {
    const customDomains = ['skiff.money', 'skiff.earth'];
    
    it('check getMailDomain (town/city/com) works', () => {
      const address = '[email protected]';
      expect(isSkiffAddress(address, customDomains)).toBe(true);
    });

    it('check skiff custom domains', () => {
      const address = '[email protected]';
      expect(isSkiffAddress(address, customDomains)).toBe(true);
    });

    it('check non skiff domain', () => {
      const address = '[email protected]';
      expect(isSkiffAddress(address, customDomains)).toBe(false);
    });
  });
});

Testing React Components

import { render, screen } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('renders with correct text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('calls onClick when clicked', () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);
    
    screen.getByText('Click me').click();
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

Testing Utilities

import { formatDate } from './dateUtils';

describe('formatDate', () => {
  it('formats date correctly', () => {
    const date = new Date('2024-03-11');
    expect(formatDate(date)).toBe('March 11, 2024');
  });

  it('handles invalid dates', () => {
    expect(formatDate(null)).toBe('');
  });
});

Mocking

Mocking Modules

jest.mock('skiff-front-utils', () => ({
  formatEmailAddress: jest.fn((email) => email),
  useCurrentUser: jest.fn(() => ({ id: 'test-user' }))
}));

Mocking API Calls (MSW)

calendar-web uses MSW for API mocking:
import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/events', (req, res, ctx) => {
    return res(ctx.json({ events: [] }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Mocking Browser APIs

// Mock localStorage
const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  clear: jest.fn()
};
global.localStorage = localStorageMock as any;

// Mock IndexedDB (calendar-web)
import 'fake-indexeddb/auto';

// Mock canvas (skemail-web)
import 'jest-canvas-mock';

Test Patterns

Arrange-Act-Assert

it('updates email subject', () => {
  // Arrange
  const initialSubject = 'Hello';
  const newSubject = 'Hello World';
  
  // Act
  const result = updateSubject(initialSubject, newSubject);
  
  // Assert
  expect(result).toBe(newSubject);
});

Testing Redux State

import { configureStore } from '@reduxjs/toolkit';
import { mailReducer } from './mailSlice';

describe('mailSlice', () => {
  let store;

  beforeEach(() => {
    store = configureStore({ reducer: { mail: mailReducer } });
  });

  it('updates selected thread', () => {
    store.dispatch(selectThread('thread-123'));
    expect(store.getState().mail.selectedThread).toBe('thread-123');
  });
});

Testing Styled Components

import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';
import { theme } from './theme';

const renderWithTheme = (component) => {
  return render(
    <ThemeProvider theme={theme}>
      {component}
    </ThemeProvider>
  );
};

it('applies correct theme colors', () => {
  const { container } = renderWithTheme(<Button>Test</Button>);
  expect(container.firstChild).toHaveStyle('color: #333');
});

Code Coverage

Running Coverage Reports

# Generate coverage report
yarn workspace skemail-web test --coverage

# View coverage in browser
open coverage/lcov-report/index.html

Coverage Thresholds

Configure in jest.config.js:
module.exports = {
  coverageThreshold: {
    global: {
      statements: 70,
      branches: 60,
      functions: 70,
      lines: 70
    }
  }
};

Continuous Integration

Tests run automatically on:
  • Pull Requests: All tests must pass before merging
  • Commits to main: Ensures main branch stability

CI Commands

The CI pipeline runs:
# Type checking
yarn typescript

# Linting
yarn lint

# Tests
yarn test

Best Practices

Focus on testing what the component does, not how it does it:
// Good: Tests behavior
it('displays error message when email is invalid', () => {
  render(<EmailInput value="invalid" />);
  expect(screen.getByText('Invalid email')).toBeInTheDocument();
});

// Avoid: Tests implementation
it('calls validateEmail function', () => {
  const spy = jest.spyOn(utils, 'validateEmail');
  render(<EmailInput value="test" />);
  expect(spy).toHaveBeenCalled();
});
Test names should clearly describe what’s being tested:
// Good
it('displays loading spinner while fetching emails', () => { });

// Avoid
it('works correctly', () => { });
Each test should be independent:
// Use beforeEach for setup
beforeEach(() => {
  localStorage.clear();
  jest.clearAllMocks();
});

// Clean up after tests
afterEach(() => {
  cleanup();
});
Don’t just test the happy path:
describe('validateEmail', () => {
  it('accepts valid email', () => { });
  it('rejects email without @', () => { });
  it('rejects email without domain', () => { });
  it('handles empty string', () => { });
  it('handles null value', () => { });
});
Follow Testing Library best practices:
// Priority order:
screen.getByRole('button', { name: /submit/i })
screen.getByLabelText('Email address')
screen.getByPlaceholderText('Enter email')
screen.getByText('Submit')
screen.getByTestId('submit-button') // Last resort

Debugging Tests

Running Single Test

# Run specific test file
yarn test userUtils.test.ts

# Run tests matching pattern
yarn test --testNamePattern="formatEmailAddress"

Debug Output

import { screen, debug } from '@testing-library/react';

it('debugging test', () => {
  render(<Component />);
  
  // Print entire DOM
  screen.debug();
  
  // Print specific element
  screen.debug(screen.getByRole('button'));
});

VS Code Debugging

Add to .vscode/launch.json:
{
  "type": "node",
  "request": "launch",
  "name": "Jest Current File",
  "program": "${workspaceFolder}/node_modules/.bin/jest",
  "args": ["${file}", "--runInBand"],
  "console": "integratedTerminal"
}

Resources

Jest Documentation

Official Jest documentation

Testing Library

React Testing Library guide

Testing Best Practices

Common testing mistakes to avoid

MSW Documentation

Mock Service Worker docs

Next Steps

Development Setup

Set up your development environment

Contributing

Learn how to contribute

Build docs developers (and LLMs) love