Skip to content

Latest commit

 

History

History
395 lines (313 loc) · 10 KB

File metadata and controls

395 lines (313 loc) · 10 KB

Testing Contributing Guidelines

Test Development Practices

Writing Effective Tests

Test Structure

Follow the Arrange-Act-Assert pattern:

test('should validate user input correctly', async () => {
  // Arrange
  const validInput = 'test@example.com';
  const invalidInput = 'not-an-email';

  // Act
  const validResult = await validateInput(validInput);
  const invalidResult = await validateInput(invalidInput);

  // Assert
  expect(validResult.isValid).toBe(true);
  expect(invalidResult.isValid).toBe(false);
  expect(invalidResult.error).toContain('Invalid email');
});

Descriptive Test Names

// ❌ Poor test names
test('crypto test');
test('it works');

// ✅ Good test names
test('should generate unique ephemeral keypairs for each pairing request');
test('should throw error when encrypting with invalid public key');
test('should handle network timeout during key exchange gracefully');

Test Categories

Use describe blocks to organize related tests:

describe('PairingFlow Component', () => {
  describe('Initial State', () => {
    test('should show initiator option by default');
    test('should have all required buttons disabled initially');
  });

  describe('Responder Flow', () => {
    test('should validate pairing code format');
    test('should show error for expired codes');
  });

  describe('Error Handling', () => {
    test('should recover from network failures');
    test('should timeout after maximum wait time');
  });
});

Security Test Requirements

Cryptographic Functions

Every cryptographic function must test:

  • Valid input handling
  • Invalid input rejection
  • Error conditions
  • Key generation uniqueness
  • Proper cleanup of sensitive data
describe('crypto service security', () => {
  test('should generate unique keypairs', async () => {
    const keys1 = await crypto.generateKeypair();
    const keys2 = await crypto.generateKeypair();

    expect(keys1.privateKey).not.toBe(keys2.privateKey);
    expect(keys1.publicKey).not.toBe(keys2.publicKey);
  });

  test('should reject malformed public keys', async () => {
    await expect(crypto.encrypt('data', 'invalid-key'))
      .rejects.toThrow('Invalid public key');
  });

  test('should clear sensitive data from memory', async () => {
    const keys = await crypto.generateEphemeralKeypair();

    await crypto.clearEphemeralKeys();

    // Verify keys are no longer accessible
    expect(() => crypto.getEphemeralPrivateKey())
      .toThrow('No ephemeral keys available');
  });
});

Pairing Protocol Security

Test all security aspects of device pairing:

describe('pairing protocol security', () => {
  test('should reject expired pairing codes', async () => {
    const code = await pairingCode.generate();

    // Mock time passage
    vi.setSystemTime(new Date(Date.now() + 11 * 60 * 1000)); // 11 minutes

    await expect(pairingCode.validate(code))
      .rejects.toThrow('Pairing code expired');
  });

  test('should limit pairing attempts', async () => {
    const invalidCode = 'INVALID';

    // Try multiple invalid attempts
    for (let i = 0; i < 5; i++) {
      await expect(pairingCode.validate(invalidCode))
        .rejects.toThrow('Invalid pairing code');
    }

    // Should be rate limited
    await expect(pairingCode.validate(invalidCode))
      .rejects.toThrow('Too many attempts');
  });
});

Component Testing Best Practices

User-Centric Testing

Test components from the user's perspective:

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

test('user can add bookmark with all details', async () => {
  const user = userEvent.setup();

  render(<BookmarkForm onSave={mockSave} />);

  // User actions
  await user.type(screen.getByLabelText(/title/i), 'My Bookmark');
  await user.type(screen.getByLabelText(/url/i), 'https://example.com');
  await user.type(screen.getByLabelText(/description/i), 'Great resource');
  await user.type(screen.getByLabelText(/tags/i), 'learning, reference');

  await user.click(screen.getByRole('button', { name: /save/i }));

  // Verify behavior
  expect(mockSave).toHaveBeenCalledWith({
    title: 'My Bookmark',
    url: 'https://example.com',
    description: 'Great resource',
    tags: ['learning', 'reference']
  });
});

Accessibility Testing

Include accessibility checks in component tests:

import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('bookmark form is accessible', async () => {
  const { container } = render(<BookmarkForm />);

  // Check for accessibility violations
  const results = await axe(container);
  expect(results).toHaveNoViolations();

  // Test keyboard navigation
  const titleInput = screen.getByLabelText(/title/i);
  await user.tab();
  expect(titleInput).toHaveFocus();
});

Error State Testing

Always test error conditions:

test('shows validation errors for invalid input', async () => {
  const user = userEvent.setup();

  render(<BookmarkForm onSave={mockSave} />);

  // Submit without required fields
  await user.click(screen.getByRole('button', { name: /save/i }));

  // Verify error messages
  expect(screen.getByText(/title is required/i)).toBeInTheDocument();
  expect(screen.getByText(/url is required/i)).toBeInTheDocument();

  // Verify form was not submitted
  expect(mockSave).not.toHaveBeenCalled();
});

Mock and Stub Guidelines

Service Mocking

// Good mocking practice
vi.mock('../services/crypto', () => ({
  generateKeypair: vi.fn().mockResolvedValue({
    publicKey: 'mock-public-key',
    privateKey: 'mock-private-key'
  }),
  encrypt: vi.fn().mockResolvedValue('encrypted-data'),
  decrypt: vi.fn().mockResolvedValue('decrypted-data')
}));

// Reset mocks between tests
beforeEach(() => {
  vi.clearAllMocks();
});

Network Mocking

// Mock fetch for API calls
global.fetch = vi.fn().mockImplementation((url) => {
  if (url.includes('/api/bookmarks')) {
    return Promise.resolve({
      ok: true,
      json: () => Promise.resolve([])
    });
  }

  return Promise.reject(new Error('Unmocked URL'));
});

Time Mocking

// Use fake timers for time-dependent tests
beforeEach(() => {
  vi.useFakeTimers();
});

afterEach(() => {
  vi.useRealTimers();
});

test('expires after timeout', async () => {
  const promise = serviceWithTimeout();

  // Advance time
  vi.advanceTimersByTime(30000); // 30 seconds

  await expect(promise).rejects.toThrow('Timeout');
});

Code Coverage Guidelines

Coverage Targets

  • New Code: 90%+ coverage required
  • Security Code: 95%+ coverage required
  • Critical Paths: 100% coverage preferred
  • Overall Project: Maintain 80%+ coverage

Coverage Quality

Focus on meaningful coverage:

// ❌ Poor coverage - only tests happy path
test('saves bookmark', () => {
  const bookmark = { title: 'Test', url: 'https://test.com' };
  const result = saveBookmark(bookmark);
  expect(result).toBe(true);
});

// ✅ Good coverage - tests multiple scenarios
describe('saveBookmark', () => {
  test('saves valid bookmark successfully', () => {
    const bookmark = { title: 'Test', url: 'https://test.com' };
    expect(saveBookmark(bookmark)).toBe(true);
  });

  test('throws error for invalid URL', () => {
    const bookmark = { title: 'Test', url: 'invalid-url' };
    expect(() => saveBookmark(bookmark)).toThrow('Invalid URL');
  });

  test('handles storage failures gracefully', () => {
    // Mock storage failure
    vi.spyOn(localStorage, 'setItem').mockImplementation(() => {
      throw new Error('Storage full');
    });

    const bookmark = { title: 'Test', url: 'https://test.com' };
    expect(() => saveBookmark(bookmark)).toThrow('Storage full');
  });
});

Coverage Exceptions

Use /* c8 ignore */ sparingly and document why:

// ✅ Acceptable - development only
/* c8 ignore next 3 */
if (process.env.NODE_ENV === 'development') {
  console.log('Debug information:', data);
}

// ✅ Acceptable - unreachable error case
/* c8 ignore next 2 */
default:
  throw new Error('Unreachable code');

// ❌ Not acceptable - should be tested
/* c8 ignore next 5 */
if (userRole === 'admin') {
  return performAdminAction();
}

Continuous Integration

Pre-commit Checks

Ensure tests pass before committing:

# Run in pre-commit hook
npm run test:coverage:check
npm run test:security
npm run lint
npm run format:check

PR Requirements

Every pull request must:

  1. Pass all existing tests
  2. Add tests for new functionality
  3. Maintain coverage thresholds
  4. Update documentation if needed

Test Maintenance

Regular Maintenance Tasks

  • Review and update test data
  • Remove obsolete tests
  • Refactor duplicated test code
  • Monitor and fix flaky tests

Flaky Test Management

// Mark known flaky tests
test.skip('flaky test - investigating timeout issues', () => {
  // Test implementation
});

// Or retry flaky tests
test.describe.configure({ retries: 2 });

test('potentially flaky network test', async () => {
  // Implementation with retries
});

Review Checklist

For Test Authors

Before submitting:

  • Tests follow naming conventions
  • All code paths are tested
  • Error conditions are handled
  • Tests are focused and independent
  • Mock cleanup is proper
  • Performance implications considered
  • Accessibility tested (for UI components)
  • Security aspects covered (for sensitive code)

For Test Reviewers

When reviewing:

  • Test quality and coverage
  • Appropriate test types used
  • Mock usage is reasonable
  • Tests are maintainable
  • Performance impact acceptable
  • Security considerations addressed
  • Documentation updated if needed

Resources