Skip to content

Latest commit

 

History

History
253 lines (214 loc) · 6.04 KB

testing-guidelines.md

File metadata and controls

253 lines (214 loc) · 6.04 KB

Testing Guidelines

Core Testing Principles

Twenty follows a comprehensive testing strategy across all packages, ensuring high-quality, maintainable code. This document outlines our testing conventions and best practices.

Testing Stack

Backend Testing

  • Primary Framework: Jest
  • API Testing: Supertest
  • Coverage Requirements: 80% minimum

Frontend Testing

  • Component Testing: Jest + React Testing Library
  • Visual Testing: Storybook
  • API Mocking: MSW (Mock Service Worker)

End-to-End Testing

  • Framework: Playwright
  • Coverage: Critical user journeys
  • Cross-browser testing

Test Organization

Test File Location

  • Co-locate tests with implementation files
  • Use consistent naming patterns
    src/
    ├── components/
    │   ├── UserProfile.tsx
    │   ├── UserProfile.test.tsx
    │   └── UserProfile.stories.tsx
    

Test File Naming

  • Use .test.ts(x) for unit/integration tests
  • Use .spec.ts(x) for E2E tests
  • Use .stories.tsx for Storybook stories

Unit Testing

Component Testing

  • Test behavior, not implementation
  • Use React Testing Library best practices
    // ✅ Correct
    test('displays user name when provided', () => {
      render(<UserProfile user={{ name: 'John Doe' }} />);
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });
    
    // ❌ Incorrect - Testing implementation details
    test('sets the text content', () => {
      const { container } = render(<UserProfile user={{ name: 'John Doe' }} />);
      expect(container.querySelector('h1').textContent).toBe('John Doe');
    });

Hook Testing

  • Use renderHook from @testing-library/react-hooks
  • Test all possible states
    // ✅ Correct
    test('useUser hook manages user state', () => {
      const { result } = renderHook(() => useUser());
      
      act(() => {
        result.current.setUser({ id: '1', name: 'John' });
      });
      
      expect(result.current.user).toEqual({ id: '1', name: 'John' });
    });

Mocking

  • Mock external dependencies
  • Use jest.mock for module mocking
    // ✅ Correct
    jest.mock('~/services/api', () => ({
      fetchUser: jest.fn().mockResolvedValue({ id: '1', name: 'John' }),
    }));
    
    test('fetches and displays user', async () => {
      render(<UserProfile userId="1" />);
      expect(await screen.findByText('John')).toBeInTheDocument();
    });

Integration Testing

API Testing

  • Test complete request/response cycles
  • Use Supertest for backend API testing
    // ✅ Correct
    describe('GET /api/users/:id', () => {
      it('returns user when found', async () => {
        const response = await request(app)
          .get('/api/users/1')
          .expect(200);
        
        expect(response.body).toEqual({
          id: '1',
          name: 'John Doe',
        });
      });
    
      it('returns 404 when user not found', async () => {
        await request(app)
          .get('/api/users/999')
          .expect(404);
      });
    });

E2E Testing

Test Structure

  • Organize by user journey
  • Use page objects for reusability
    // pages/login.ts
    export class LoginPage {
      async login(email: string, password: string) {
        await this.page.fill('[data-testid="email-input"]', email);
        await this.page.fill('[data-testid="password-input"]', password);
        await this.page.click('[data-testid="login-button"]');
      }
    }
    
    // tests/auth.spec.ts
    test('user can login successfully', async ({ page }) => {
      const loginPage = new LoginPage(page);
      await loginPage.login('[email protected]', 'password');
      await expect(page).toHaveURL('/dashboard');
    });

Test Data

  • Use dedicated test environments
  • Reset state between tests
    // ✅ Correct
    beforeEach(async () => {
      await resetDatabase();
      await seedTestData();
    });
    
    test('user workflow', async ({ page }) => {
      // Test with clean, predictable state
    });

Visual Testing

Storybook Guidelines

  • Create stories for all components
  • Document component variants
    // Button.stories.tsx
    export default {
      title: 'Components/Button',
      component: Button,
    } as Meta;
    
    export const Primary = {
      args: {
        variant: 'primary',
        label: 'Primary Button',
      },
    };
    
    export const Secondary = {
      args: {
        variant: 'secondary',
        label: 'Secondary Button',
      },
    };

Visual Regression

  • Use Storybook's visual regression testing
  • Review changes carefully
    // jest.config.js
    module.exports = {
      preset: 'jest-image-snapshot',
      setupFilesAfterEnv: ['<rootDir>/setup-tests.ts'],
    };
    
    // Button.visual.test.tsx
    describe('Button', () => {
      it('matches visual snapshot', async () => {
        const image = await page.screenshot();
        expect(image).toMatchImageSnapshot();
      });
    });

Test Quality

Test Data Attributes

  • Use data-testid for test selectors
  • Avoid selecting by CSS classes
    // ✅ Correct
    <button data-testid="submit-button">Submit</button>
    
    // In tests
    const button = screen.getByTestId('submit-button');
    
    // ❌ Incorrect
    const button = container.querySelector('.submit-btn');

Assertion Best Practices

  • Use explicit assertions
  • Test both positive and negative cases
    // ✅ Correct
    test('form validation', async () => {
      render(<UserForm />);
      
      // Negative case
      await userEvent.click(screen.getByText('Submit'));
      expect(screen.getByText('Name is required')).toBeInTheDocument();
      
      // Positive case
      await userEvent.type(screen.getByLabelText('Name'), 'John Doe');
      await userEvent.click(screen.getByText('Submit'));
      expect(screen.queryByText('Name is required')).not.toBeInTheDocument();
    });

Coverage Requirements

  • A new feature should have at least 80% coverage
  • Focus on critical paths
  • Run coverage reports in CI
    // jest.config.js
    module.exports = {
      coverageThreshold: {
        global: {
          statements: 80,
          branches: 80,
          functions: 80,
          lines: 80,
        },
      },
    };