10 KiB
10 KiB
Tester Subagent Reference
Critical Rule: Linux Only (ADR-014)
ALL tests MUST run in the dev container. Windows test results are unreliable.
| Result | Interpretation |
|---|---|
| Pass Windows / Fail Linux | BROKEN - must fix |
| Fail Windows / Pass Linux | PASSING - acceptable |
Test Commands
From Windows Host (via Podman)
# Unit tests (~2900 tests) - pipe to file for AI processing
podman exec -it flyer-crawler-dev npm run test:unit 2>&1 | tee test-results.txt
# Integration tests (requires DB/Redis)
podman exec -it flyer-crawler-dev npm run test:integration
# E2E tests (requires all services)
podman exec -it flyer-crawler-dev npm run test:e2e
# Specific test file
podman exec -it flyer-crawler-dev npm test -- --run src/hooks/useAuth.test.tsx
# Type checking (CRITICAL before commit)
podman exec -it flyer-crawler-dev npm run type-check
# Coverage report
podman exec -it flyer-crawler-dev npm run test:coverage
Inside Dev Container
npm test # All tests
npm run test:unit # Unit tests only
npm run test:integration # Integration tests
npm run test:e2e # E2E tests
npm run type-check # TypeScript check
Test File Locations
| Test Type | Location | Config |
|---|---|---|
| Unit | src/**/*.test.ts, src/**/*.test.tsx |
vite.config.ts |
| Integration | src/tests/integration/*.integration.test.ts |
vitest.config.integration.ts |
| E2E | src/tests/e2e/*.e2e.test.ts |
vitest.config.e2e.ts |
| Setup | src/tests/setup/*.ts |
- |
| Helpers | src/tests/utils/*.ts |
- |
Test Helpers
Location: src/tests/utils/
| Helper | Purpose | Import |
|---|---|---|
testHelpers.ts |
createAndLoginUser(), getTestBaseUrl(), getFlyerBaseUrl() |
../tests/utils/testHelpers |
cleanup.ts |
cleanupDb({ userIds, flyerIds }) |
../tests/utils/cleanup |
mockFactories.ts |
createMockStore(), createMockAddress(), createMockFlyer() |
../tests/utils/mockFactories |
storeHelpers.ts |
createStoreWithLocation(), cleanupStoreLocations() |
../tests/utils/storeHelpers |
poll.ts |
poll(fn, predicate, options) - wait for async conditions |
../tests/utils/poll |
mockLogger.ts |
Mock pino logger for tests | ../tests/utils/mockLogger |
createTestApp.ts |
Create Express app instance for route tests | ../tests/utils/createTestApp |
createMockRequest.ts |
Create mock Express request objects | ../tests/utils/createMockRequest |
cleanupFiles.ts |
Clean up test file uploads | ../tests/utils/cleanupFiles |
websocketTestUtils.ts |
WebSocket testing utilities | ../tests/utils/websocketTestUtils |
Usage Examples
// Create authenticated user for tests
import { createAndLoginUser, TEST_PASSWORD } from '../tests/utils/testHelpers';
const { user, token } = await createAndLoginUser({
email: `test-${Date.now()}@example.com`,
request: request(app), // For integration tests
role: 'admin', // Optional: make admin
});
// Cleanup after tests
import { cleanupDb } from '../tests/utils/cleanup';
afterEach(async () => {
await cleanupDb({ userIds: [user.user.user_id] });
});
// Wait for async operation
import { poll } from '../tests/utils/poll';
await poll(
() => db.userRepo.findUserByEmail(email, logger),
(user) => !!user,
{ timeout: 5000, interval: 500, description: 'user to be findable' },
);
// Create mock data
import { createMockStore, createMockFlyer } from '../tests/utils/mockFactories';
const mockStore = createMockStore({ name: 'Test Store' });
const mockFlyer = createMockFlyer({ store_id: mockStore.store_id });
Test Setup Files
| File | Purpose |
|---|---|
src/tests/setup/tests-setup-unit.ts |
Unit test setup (mocks, DOM environment) |
src/tests/setup/tests-setup-integration.ts |
Integration test setup (DB connections) |
src/tests/setup/global-setup.ts |
Global setup for unit tests |
src/tests/setup/integration-global-setup.ts |
Global setup for integration tests |
src/tests/setup/e2e-global-setup.ts |
Global setup for E2E tests |
src/tests/setup/mockHooks.ts |
React hook mocking utilities |
src/tests/setup/mockUI.ts |
UI component mocking |
src/tests/setup/globalApiMock.ts |
API mocking setup |
Known Integration Test Issues
1. Vitest globalSetup Context Isolation
Problem: globalSetup runs in separate Node.js context. Singletons/mocks don't share instances.
Affected: BullMQ worker mocks
Solution: Use .todo(), test-only API endpoints, or Redis-based flags.
2. Cleanup Queue Race Condition
Problem: Cleanup worker processes jobs before test verification.
Solution:
const { cleanupQueue } = await import('../../services/queues.server');
await cleanupQueue.drain();
await cleanupQueue.pause();
// ... run test ...
await cleanupQueue.resume();
3. Cache Stale After Direct SQL
Problem: Direct pool.query() bypasses cache invalidation.
Solution:
await pool.query('INSERT INTO flyers ...');
await cacheService.invalidateFlyers(); // Add this
4. File Upload Filename Collisions
Problem: Multer predictable filenames cause race conditions.
Solution:
const filename = `test-${Date.now()}-${Math.round(Math.random() * 1e9)}.jpg`;
5. Response Format Mismatches
Problem: API response structure changes (data.jobId vs data.job.id).
Solution: Log response bodies, update assertions to match actual format.
Test Patterns
Unit Test Pattern
import { describe, it, expect, vi, beforeEach } from 'vitest';
describe('MyService', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should do something', () => {
// Arrange
const input = { data: 'test' };
// Act
const result = myService.process(input);
// Assert
expect(result).toBe('expected');
});
});
Integration Test Pattern
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import request from 'supertest';
import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
describe('API Integration', () => {
let user, token;
beforeEach(async () => {
const result = await createAndLoginUser({ request: request(app) });
user = result.user;
token = result.token;
});
afterEach(async () => {
await cleanupDb({ userIds: [user.user.user_id] });
});
it('GET /api/resource returns data', async () => {
const res = await request(app).get('/api/resource').set('Authorization', `Bearer ${token}`);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
});
});
Route Test Pattern
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { createTestApp } from '../tests/utils/createTestApp';
vi.mock('../services/db/flyer.db', () => ({
getFlyerById: vi.fn(),
}));
describe('Flyer Routes', () => {
let app;
beforeEach(() => {
vi.clearAllMocks();
app = createTestApp();
});
it('GET /api/flyers/:id returns flyer', async () => {
const mockFlyer = { id: '123', name: 'Test' };
vi.mocked(flyerRepo.getFlyerById).mockResolvedValue(mockFlyer);
const res = await request(app).get('/api/flyers/123');
expect(res.status).toBe(200);
expect(res.body.data).toEqual(mockFlyer);
});
});
Mocking Patterns
Mock Modules
// At top of test file
vi.mock('../services/db/flyer.db', () => ({
getFlyerById: vi.fn(),
listFlyers: vi.fn(),
}));
// In test
import * as flyerDb from '../services/db/flyer.db';
vi.mocked(flyerDb.getFlyerById).mockResolvedValue(mockFlyer);
Mock React Query
vi.mock('@tanstack/react-query', async () => {
const actual = await vi.importActual('@tanstack/react-query');
return {
...actual,
useQuery: vi.fn().mockReturnValue({
data: mockData,
isLoading: false,
error: null,
}),
};
});
Mock Pino Logger
import { createMockLogger } from '../tests/utils/mockLogger';
const mockLogger = createMockLogger();
Testing React Components
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return ({ children }) => (
<QueryClientProvider client={queryClient}>
<BrowserRouter>{children}</BrowserRouter>
</QueryClientProvider>
);
};
it('renders component', () => {
render(<MyComponent />, { wrapper: createWrapper() });
expect(screen.getByText('Expected Text')).toBeInTheDocument();
});
Test Coverage
# Generate coverage report
podman exec -it flyer-crawler-dev npm run test:coverage
# View HTML report
# Coverage reports generated in coverage/ directory
Debugging Tests
# Verbose output
npm test -- --reporter=verbose
# Run single test with debugging
DEBUG=* npm test -- --run src/path/to/test.test.ts
# Vitest UI (interactive)
npm run test:ui