APP

Documentation

Developer Guide

Testing Infrastructure

AutoTechJobs includes a comprehensive testing setup to ensure code quality and reliability. The testing strategy covers multiple levels from unit tests to integration tests.

Testing Stack

The application uses the following testing tools:

  • Vitest - Fast and lightweight test runner compatible with Vite
  • Testing Library - Utilities for testing React components
  • MSW (Mock Service Worker) - API mocking for browser and Node.js
  • Playwright - End-to-end testing framework

Test Types

Unit Tests

Testing individual components and functions in isolation:

// Example unit test for a utility function
import { describe, it, expect } from 'vitest';
import { formatCurrency } from '~/lib/utils/formatters';

describe('formatCurrency', () => {
  it('formats currency correctly', () => {
    expect(formatCurrency(1000)).toBe('$1,000.00');
    expect(formatCurrency(1000.5)).toBe('$1,000.50');
    expect(formatCurrency(0)).toBe('$0.00');
  });

  it('handles negative values', () => {
    expect(formatCurrency(-1000)).toBe('-$1,000.00');
  });
});

Component Tests

Testing React components with Testing Library:

// Example component test
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import JobCard from '~/components/JobCard';

describe('JobCard', () => {
  const mockJob = {
    id: '1',
    title: 'Automotive Technician',
    company: 'ABC Motors',
    location: 'New York, NY',
    salary: '$60,000 - $80,000',
    postedDate: '2025-01-15'
  };

  it('renders job details correctly', () => {
    render(<JobCard job={mockJob} />);
    
    expect(screen.getByText('Automotive Technician')).toBeInTheDocument();
    expect(screen.getByText('ABC Motors')).toBeInTheDocument();
    expect(screen.getByText('New York, NY')).toBeInTheDocument();
    expect(screen.getByText('$60,000 - $80,000')).toBeInTheDocument();
  });

  it('navigates to job details when clicked', async () => {
    const user = userEvent.setup();
    render(<JobCard job={mockJob} />);
    
    const card = screen.getByRole('link');
    await user.click(card);
    
    // Assert navigation or click handler was called
  });
});

Route Tests

Testing Remix routes with loaders and actions:

// Example route test
import { describe, it, expect, vi } from 'vitest';
import { createRemixStub } from '@remix-run/testing';
import { render, screen } from '@testing-library/react';
import { loader, action } from '~/routes/job.$jobId';
import JobDetailRoute from '~/routes/job.$jobId';

vi.mock('~/lib/repositories/job.repo', () => ({
  JobRepository: vi.fn().mockImplementation(() => ({
    findById: vi.fn().mockResolvedValue({
      id: '1',
      title: 'Automotive Technician',
      description: 'Job description...',
      // Other job fields
    })
  }))
}));

describe('Job Detail Route', () => {
  it('loads job data correctly', async () => {
    const RemixStub = createRemixStub([
      {
        path: '/job/:jobId',
        element: <JobDetailRoute />,
        loader,
        action
      }
    ]);

    render(
      <RemixStub initialEntries={['/job/1']} />
    );

    // Wait for data to load
    expect(await screen.findByText('Automotive Technician')).toBeInTheDocument();
    expect(screen.getByText('Job description...')).toBeInTheDocument();
  });
});

Repository Tests

Testing database interactions with mocked D1 database:

// Example repository test
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { JobRepository } from '~/lib/repositories/job.repo';

describe('JobRepository', () => {
  let mockDb;
  let jobRepository;

  beforeEach(() => {
    // Mock D1 database methods
    mockDb = {
      prepare: vi.fn().mockReturnThis(),
      bind: vi.fn().mockReturnThis(),
      first: vi.fn().mockResolvedValue({ id: '1', title: 'Test Job' }),
      all: vi.fn().mockResolvedValue({ results: [{ id: '1', title: 'Test Job' }] }),
      run: vi.fn().mockResolvedValue({ success: true })
    };
    
    jobRepository = new JobRepository(mockDb);
  });

  it('finds job by id', async () => {
    const job = await jobRepository.findById('1');
    
    expect(mockDb.prepare).toHaveBeenCalledWith('SELECT * FROM jobs WHERE id = ?');
    expect(mockDb.bind).toHaveBeenCalledWith('1');
    expect(job).toEqual({ id: '1', title: 'Test Job' });
  });

  it('creates a new job', async () => {
    const jobData = { title: 'New Job', description: 'Description' };
    await jobRepository.create(jobData);
    
    expect(mockDb.prepare).toHaveBeenCalled();
    expect(mockDb.bind).toHaveBeenCalled();
    expect(mockDb.run).toHaveBeenCalled();
  });
});

End-to-End Tests

Testing complete user flows with Playwright:

// Example E2E test
import { test, expect } from '@playwright/test';

test('job application flow', async ({ page }) => {
  // Login as a job seeker
  await page.goto('/login');
  await page.fill('input[name="email"]', '[email protected]');
  await page.fill('input[name="password"]', 'password123');
  await page.click('button[type="submit"]');
  
  // Search for a job
  await page.goto('/search');
  await page.fill('input[name="keyword"]', 'mechanic');
  await page.click('button[type="submit"]');
  
  // Click on the first job result
  await page.click('.job-card:first-child');
  
  // Apply for the job
  await page.click('button:has-text("Apply Now")');
  await page.fill('textarea[name="coverLetter"]', 'I am interested in this position...');
  await page.selectOption('select[name="resumeVersion"]', '1');
  await page.click('button[type="submit"]:has-text("Submit Application")');
  
  // Verify success message
  await expect(page.locator('.success-message')).toContainText('Application submitted');
  
  // Verify application appears in dashboard
  await page.goto('/candidates/dashboard/applications');
  await expect(page.locator('.application-list')).toContainText('mechanic');
});

Test Organization

Tests are organized in a structure that mirrors the application code:

test/
├── unit/                 # Unit tests
│   ├── utils/            # Utility function tests
│   └── validation/       # Validation logic tests
├── components/           # Component tests
│   ├── JobCard.test.tsx
│   └── ...
├── routes/               # Route tests
│   ├── job.$jobId.test.tsx
│   └── ...
├── repositories/         # Repository tests
│   ├── job.repo.test.ts
│   └── ...
├── services/             # Service tests
│   ├── auth.service.test.ts
│   └── ...
└── e2e/                  # End-to-end tests
    ├── job-application.spec.ts
    └── ...

Continuous Integration

Tests are automatically run in the CI/CD pipeline using GitHub Actions:

# .github/workflows/test.yml
name: Test

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm run test:unit
      - run: npm run test:e2e
      - name: Upload test coverage
        uses: codecov/codecov-action@v3

Testing Best Practices

  • Write tests for critical business logic and user flows
  • Use meaningful test descriptions that document behavior
  • Maintain test isolation to prevent interdependencies
  • Mock external dependencies consistently
  • Aim for high test coverage of core functionality
  • Include accessibility testing in component tests
  • Run tests locally before pushing code