Testing Patterns & Strategies

Unit Testing Pattern

import { describe, it, expect, vi, beforeEach } from 'vitest';

describe('UnitBeing Tested', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('SpecificFeature', () => {
    it('should do X when given Y', () => {
      // Arrange
      const input = setupTestData();
      
      // Act
      const result = functionUnderTest(input);
      
      // Assert
      expect(result).toBe(expectedOutput);
    });

    it('handles error scenario', () => {
      const error = new Error('Test error');
      vi.mocked(dependency).mockRejectedValue(error);
      
      expect(() => functionUnderTest()).toThrow(error);
    });
  });
});

Component Testing Pattern

import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('Component', () => {
  it('renders with required props', () => {
    render(<Component required="value" />);
    expect(screen.getByRole('button')).toBeInTheDocument();
  });

  it('calls onClick when button clicked', async () => {
    const handleClick = vi.fn();
    render(<Component onClick={handleClick} />);
    
    await userEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalled();
  });

  it('applies className variants', () => {
    const { container } = render(<Component variant="primary" />);
    expect(container.firstChild).toHaveClass('component--primary');
  });
});

Integration Testing Pattern

describe('Integration: Product Workflow', () => {
  it('creates, updates, and deletes product', async () => {
    // Mock API responses
    const mockProduct = { id: '1', name: 'Test' };
    vi.mocked(apiClient.post).mockResolvedValue({ data: mockProduct });
    vi.mocked(apiClient.put).mockResolvedValue({ 
      data: { ...mockProduct, name: 'Updated' } 
    });
    vi.mocked(apiClient.delete).mockResolvedValue({ status: 204 });

    // Execute workflow
    const created = await ProductService.createProduct(data);
    const updated = await ProductService.updateProduct(created.id, updates);
    await ProductService.deleteProduct(created.id);

    // Verify calls
    expect(apiClient.post).toHaveBeenCalled();
    expect(apiClient.put).toHaveBeenCalled();
    expect(apiClient.delete).toHaveBeenCalled();
  });
});

Accessibility Testing Pattern

describe('Accessibility', () => {
  it('button has proper ARIA label', () => {
    render(<Button aria-label="Close dialog">×</Button>);
    expect(screen.getByLabelText('Close dialog')).toBeInTheDocument();
  });

  it('form inputs are keyboard accessible', async () => {
    render(<Form />);
    
    const firstInput = screen.getByLabelText('Name');
    const secondInput = screen.getByLabelText('Email');
    
    firstInput.focus();
    await userEvent.keyboard('{Tab}');
    
    expect(document.activeElement).toBe(secondInput);
  });
});

Hook Testing Pattern

import { renderHook, act, waitFor } from '@testing-library/react';

describe('useCustomHook', () => {
  it('returns initial state', () => {
    const { result } = renderHook(() => useCustomHook());
    expect(result.current.value).toBe(initialValue);
  });

  it('updates state on action', async () => {
    const { result } = renderHook(() => useCustomHook());
    
    act(() => {
      result.current.setValue('newValue');
    });
    
    expect(result.current.value).toBe('newValue');
  });

  it('fetches data on mount', async () => {
    const { result } = renderHook(() => useDataFetch());
    
    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });
    
    expect(result.current.data).toBeDefined();
  });
});

Error Testing Pattern

describe('Error Handling', () => {
  it('catches and logs errors', async () => {
    const consoleSpy = vi.spyOn(console, 'error').mockImplementation();
    
    try {
      await functionThatThrows();
    } catch (error) {
      expect(error).toBeInstanceOf(Error);
      expect(consoleSpy).toHaveBeenCalled();
    }
    
    consoleSpy.mockRestore();
  });

  it('shows error message to user', async () => {
    vi.mocked(api.fetch).mockRejectedValue(new Error('Network failed'));
    
    const { result } = renderHook(() => useData());
    
    await waitFor(() => {
      expect(result.current.error).toBeTruthy();
    });
  });
});

Snapshot Testing Pattern

describe('Snapshots', () => {
  it('matches snapshot', () => {
    const { container } = render(<Component prop="value" />);
    expect(container.firstChild).toMatchSnapshot();
  });

  it('snapshot with different variants', () => {
    ['primary', 'secondary'].forEach(variant => {
      const { container } = render(<Button variant={variant} />);
      expect(container.firstChild).toMatchSnapshot(variant);
    });
  });
});

Mocking Patterns

Mock Service

vi.mock('@/api/ProductService', () => ({
  ProductService: {
    getProducts: vi.fn(),
    createProduct: vi.fn()
  }
}));

Mock Custom Hook

vi.mock('@/services/hooks/useProducts', () => ({
  useProducts: vi.fn().mockReturnValue({
    products: [],
    loading: false,
    error: null
  })
}));

Mock Redux

import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';

const mockStore = configureStore({
  reducer: {
    products: () => ({ items: [] })
  }
});

render(
  <Provider store={mockStore}>
    <Component />
  </Provider>
);

Common Assertions

// Existence
expect(element).toBeInTheDocument();
expect(element).toBeDefined();

// Content
expect(element).toHaveTextContent('text');
expect(screen.getByText(/pattern/i)).toBeInTheDocument();

// Classes & Attributes
expect(element).toHaveClass('className');
expect(element).toHaveAttribute('disabled');

// State
expect(checkbox).toBeChecked();
expect(button).toBeDisabled();

// Counts
expect(listItems).toHaveLength(3);

// Functions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(expectedArg);
expect(mockFn).toHaveBeenCalledTimes(2);

// Errors
expect(() => throwingFn()).toThrow();
expect(promise).rejects.toThrow();

// Snapshots
expect(component).toMatchSnapshot();


Last Updated: November 2025