AutoTechJobs includes a comprehensive testing setup to ensure code quality and reliability. The testing strategy covers multiple levels from unit tests to integration tests.
The application uses the following testing tools:
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');
});
});
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
});
});
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();
});
});
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();
});
});
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');
});
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
└── ...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