| name | Testing & CI/CD |
| description | Complete guide for testing patterns, test structure, CI pipeline, and development workflow for SE104_VLEAGUE |
Testing & CI/CD Skill
Test Coverage Summary
| Area | Suites | Framework |
|---|
| API Unit + Controller | 23 | Jest + ts-jest |
| API E2E | 13 | Jest + Supertest |
| Web Unit + Component | 30 | Vitest + @testing-library/react |
Backend Testing (Jest)
Test File Structure
apps/api/src/
โโโ app.controller.spec.ts
โโโ auth/
โ โโโ auth.service.spec.ts # 30+ tests
โ โโโ auth.controller.spec.ts # 19 tests
โโโ registration/
โ โโโ registration.service.spec.ts
โ โโโ teams.controller.spec.ts # 6 tests
โ โโโ players.controller.spec.ts # 6 tests
โโโ match/
โ โโโ match.service.spec.ts
โ โโโ match.controller.spec.ts # 7 tests
โโโ scheduling/
โ โโโ scheduling.service.spec.ts
โ โโโ scheduling.controller.spec.ts # 7 tests
โโโ season/
โ โโโ season.service.spec.ts
โ โโโ season.controller.spec.ts # 8 tests
โ โโโ season-team.controller.spec.ts # 5 tests
โโโ stadium/
โ โโโ stadium.service.spec.ts
โโโ standings/
โ โโโ standings.service.spec.ts
โ โโโ standings.controller.spec.ts
โโโ roster/
โ โโโ roster.service.spec.ts
โ โโโ roster.controller.spec.ts
โโโ regulation/
โ โโโ regulation.service.spec.ts
โ โโโ regulation.controller.spec.ts # 6 tests
โโโ users/
โ โโโ users.service.spec.ts # 15 tests
โ โโโ users.controller.spec.ts # 5 tests
โโโ upload/
โโโ upload.controller.spec.ts # 6 tests
apps/api/test/ # E2E tests
โโโ app.e2e-spec.ts
โโโ auth.e2e-spec.ts
โโโ matches.e2e-spec.ts
โโโ regulations.e2e-spec.ts
โโโ roster.e2e-spec.ts
โโโ scheduling.e2e-spec.ts
โโโ seasons.e2e-spec.ts
โโโ stadiums.e2e-spec.ts
โโโ standings.e2e-spec.ts
โโโ teams.e2e-spec.ts
โโโ upload.e2e-spec.ts
โโโ users.e2e-spec.ts
โโโ jest-e2e.json
Unit Test Pattern (Service)
import { Test, TestingModule } from '@nestjs/testing';
import { ModuleService } from './module.service';
import { PrismaService } from '../prisma/prisma.service';
describe('ModuleService', () => {
let service: ModuleService;
let prisma: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ModuleService,
{
provide: PrismaService,
useValue: {
modelName: {
findMany: jest.fn(),
findUnique: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
count: jest.fn(),
},
},
},
],
}).compile();
service = .<>();
prisma = .<>();
});
(, () => {
mockData = [{ : , : }];
(prisma.. jest.).(mockData);
(prisma.. jest.).();
result = service.({ : , : });
(result.).(mockData);
(result.).();
});
});
Controller Test Pattern
describe('ModuleController', () => {
let controller: ModuleController;
let service: ModuleService;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [ModuleController],
providers: [
{
provide: ModuleService,
useValue: {
findAll: jest.fn(),
findOne: jest.fn(),
create: jest.fn(),
},
},
],
}).compile();
controller = module.get(ModuleController);
service = module.get(ModuleService);
});
it('should delegate to service', async () => {
const mockResult = { id: 'uuid-1' };
(service.findOne as jest.Mock).mockResolvedValue(mockResult);
expect(await controller.findOne()).(mockResult);
});
});
Auth Service Test Pattern
jest.mock('bcrypt');
import * as bcrypt from 'bcrypt';
describe('AuthService', () => {
it('should login successfully', async () => {
(bcrypt.compare as jest.Mock).mockResolvedValue(true);
});
});
E2E Test Pattern
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('TeamsController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
it('/api/teams (GET)', () => {
return request(app.getHttpServer()).get('/api/teams').expect(200);
});
});
Backend Test Tips
- Use
as any for Prisma mock return types when TypeScript complains
- Mock cross-module services (e.g.,
StandingsService, RegulationHelper) when testing modules that import them
- For
MatchService tests: mock StandingsService.recalculate() and RegulationHelper.getNumericValue()
- For
RosterService tests: mock RegulationHelper for MAX_ROSTER, MAX_FOREIGN_PLAYERS validation
Frontend Testing (Vitest)
Test File Structure
apps/web/src/
โโโ auth/
โ โโโ AuthContext.test.tsx
โ โโโ RequireAuth.test.tsx
โโโ pages/__tests__/
โ โโโ DashboardPage.test.tsx
โ โโโ TeamsPage.test.tsx
โ โโโ PlayersPage.test.tsx
โ โโโ SeasonsPage.test.tsx
โ โโโ MatchesPage.test.tsx
โ โโโ SchedulePage.test.tsx
โ โโโ StandingsPage.test.tsx
โ โโโ RegulationsPage.test.tsx
โ โโโ ProfilePage.test.tsx
โ โโโ LoginPage.test.tsx
โโโ services/__tests__/
โโโ authApi.test.ts
โโโ teamApi.test.ts
โโโ playerApi.test.ts
โโโ stadiumApi.test.ts
โโโ seasonApi.test.ts
โโโ seasonTeamApi.test.ts
โโโ matchApi.test.ts
โโโ scheduleApi.test.ts
โโโ standingsApi.test.ts
โโโ regulationApi.test.ts
โโโ searchApi.test.ts
โโโ userApi.test.ts
โโโ uploadApi.test.ts
Service Test Pattern
import { describe, it, expect, vi, beforeEach } from 'vitest';
const mockApi = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn(),
patch: vi.fn(),
delete: vi.fn(),
put: vi.fn(),
}));
vi.mock('../../lib/api', () => ({
default: mockApi,
}));
import { apiGetTeams, apiCreateTeam } from '../teamApi';
describe('teamApi', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should fetch teams', async () => {
const mockResponse = { data: { data: [], total: 0, page: 1, limit: 10 } };
mockApi.get.mockResolvedValue(mockResponse);
const result = await apiGetTeams({ page: 1, limit: 10 });
(mockApi.).(, { : { : , : } });
(result).(mockResponse.);
});
});
Page Component Test Pattern
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
vi.mock('../../services/teamApi', () => ({
apiGetTeams: vi.fn().mockResolvedValue({ data: [], total: 0, page: 1, limit: 10, totalPages: 0 }),
}));
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return { ...actual, useNavigate: vi.fn(() => vi.fn()) };
});
import TeamsPage from '../TeamsPage';
describe('TeamsPage', () => {
it('renders without crashing', async () => {
(
);
( {
(screen.().).();
});
});
});
Test Setup (vitest.setup.ts)
import '@testing-library/jest-dom/vitest';
import './lib/i18n';
global.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
Frontend Test Gotchas
vi.hoisted() is MANDATORY for mock variables used inside vi.mock() โ Vitest hoists vi.mock() to top of file
- Ant Design duplicate text: Use
getAllByText() instead of getByText() โ AntD often renders text in multiple DOM nodes
- Import paths: Tests in
__tests__/ must import components with ../ComponentName
- LoginPage button: The submit button renders as
<button> inside AntD Form โ query by role: getByRole('button', { name: /login/i })
- Mock individual services: Don't mock the entire services directory โ mock specific service files
- Vitest config:
environment: 'jsdom', globals: true, css: false
CI/CD Pipeline (GitHub Actions)
Workflow Structure
name: CI
on: [push, pull_request] โ main
jobs:
api-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env: POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
ports: ["5432:5432"]
steps:
- Checkout
- Setup Node.js + pnpm
- Install dependencies
- Generate Prisma client
- Run migrations (migrate deploy)
- Seed database
- Run unit tests (pnpm test)
- Run E2E tests (pnpm test:e2e)
Additional CI Features
- PR Labeler: Auto-labels PRs based on file paths
- CodeQL: Security scanning
- Dependabot: Automated dependency updates
Development Workflow
Branch Strategy
main (protected)
โโโ feature/VL-xxx-description
โโโ fix/VL-xxx-description
โโโ chore/description
Commit Convention (Conventional Commits)
feat(module): add new feature
fix(module): fix bug description
test(module): add/update tests
docs: update documentation
chore: maintenance tasks
refactor(module): code restructuring
PR Flow
- Create feature branch from
main
- Implement + add tests
- Push โ CI runs automatically
- PR review โ merge to
main
Branch Protection Rules
- Require CI passing before merge
- Require PR review
- No direct push to
main
Running Tests
cd apps/api
pnpm test
pnpm test:watch
pnpm test:cov
pnpm test:e2e
cd apps/web
pnpm test
pnpm exec vitest
pnpm exec vitest --coverage
cd apps/api && pnpm test -- auth.service.spec
cd apps/web && pnpm exec vitest src/services/__tests__/teamApi.test.ts
Troubleshooting
API Tests
- "Cannot find module 'bcrypt'": Ensure
jest.mock('bcrypt') is at module level (before imports)
- Prisma type errors in mocks: Use
as any for mock return values
- E2E tests fail: Check PostgreSQL is running and DATABASE_URL is correct
- Cross-module injection errors: Ensure mock providers include all injected services
Web Tests
- "ReferenceError: vi is not defined": Add
globals: true to vitest.config.ts
- "ResizeObserver is not defined": Check vitest.setup.ts polyfill is loaded
- "Cannot find module": Verify import paths (from
__tests__/ โ ../Component)
- Ant Design matcher failures: Use
getAllByText or queryAllByText instead of singular variants
- Mock not working: Ensure
vi.hoisted() wraps mock variables, and vi.mock() path matches import exactly