一键导入
testing-cicd
Complete guide for testing patterns, test structure, CI pipeline, and development workflow for SE104_VLEAGUE
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Complete guide for testing patterns, test structure, CI pipeline, and development workflow for SE104_VLEAGUE
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | Testing & CI/CD |
| description | Complete guide for testing patterns, test structure, CI pipeline, and development workflow for SE104_VLEAGUE |
| Area | Suites | Framework |
|---|---|---|
| API Unit + Controller | 23 | Jest + ts-jest |
| API E2E | 13 | Jest + Supertest |
| Web Unit + Component | 30 | Vitest + @testing-library/react |
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
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 = module.get<ModuleService>(ModuleService);
prisma = module.get<PrismaService>(PrismaService);
});
it('should return paginated results', async () => {
const mockData = [{ id: 'uuid-1', name: 'Test' }];
(prisma.modelName.findMany as jest.Mock).mockResolvedValue(mockData);
(prisma.modelName.count as jest.Mock).mockResolvedValue(1);
const result = await service.findAll({ page: 1, limit: 10 });
expect(result.data).toEqual(mockData);
expect(result.total).toBe(1);
});
});
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('uuid-1')).toBe(mockResult);
});
});
// IMPORTANT: Mock bcrypt at module level
jest.mock('bcrypt');
import * as bcrypt from 'bcrypt';
describe('AuthService', () => {
// ... setup with mocked PrismaService, JwtService, MailService, ConfigService
it('should login successfully', async () => {
(bcrypt.compare as jest.Mock).mockResolvedValue(true);
// ... test login flow
});
});
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);
});
});
as any for Prisma mock return types when TypeScript complainsStandingsService, RegulationHelper) when testing modules that import themMatchService tests: mock StandingsService.recalculate() and RegulationHelper.getNumericValue()RosterService tests: mock RegulationHelper for MAX_ROSTER, MAX_FOREIGN_PLAYERS validationapps/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
import { describe, it, expect, vi, beforeEach } from 'vitest';
// CRITICAL: Use vi.hoisted() for mock variables
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 });
expect(mockApi.get).toHaveBeenCalledWith('/teams', { params: { page: 1, limit: 10 } });
expect(result).toEqual(mockResponse.data);
});
});
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
// Mock services
vi.mock('../../services/teamApi', () => ({
apiGetTeams: vi.fn().mockResolvedValue({ data: [], total: 0, page: 1, limit: 10, totalPages: 0 }),
}));
// Mock react-router-dom partially
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return { ...actual, useNavigate: vi.fn(() => vi.fn()) };
});
// Import paths from __tests__/ go up one level
import TeamsPage from '../TeamsPage';
describe('TeamsPage', () => {
it('renders without crashing', async () => {
render(
<MemoryRouter>
<TeamsPage />
</MemoryRouter>
);
await waitFor(() => {
// Use getAllByText for Ant Design duplicate rendering
expect(screen.getAllByText(/teams/i).length).toBeGreaterThan(0);
});
});
});
vitest.setup.ts)import '@testing-library/jest-dom/vitest';
// i18n — Vietnam, no language detector
import './lib/i18n';
// ResizeObserver polyfill (Ant Design Tabs, Collapse)
global.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
// window.matchMedia polyfill (Ant Design responsive)
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(),
})),
});
vi.hoisted() is MANDATORY for mock variables used inside vi.mock() — Vitest hoists vi.mock() to top of filegetAllByText() instead of getByText() — AntD often renders text in multiple DOM nodes__tests__/ must import components with ../ComponentName<button> inside AntD Form — query by role: getByRole('button', { name: /login/i })environment: 'jsdom', globals: true, css: false# .github/workflows/ci.yml
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)
web-test:
runs-on: ubuntu-latest
steps:
- Checkout
- Setup Node.js + pnpm
- Install dependencies
- Run Vitest (pnpm test)
main (protected)
├── feature/VL-xxx-description
├── fix/VL-xxx-description
└── chore/description
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
mainmainmain# Backend
cd apps/api
pnpm test # Unit tests (23 suites)
pnpm test:watch # Watch mode
pnpm test:cov # Coverage report
pnpm test:e2e # E2E tests (requires DB)
# Frontend
cd apps/web
pnpm test # Vitest (30 suites)
pnpm exec vitest # Watch mode
pnpm exec vitest --coverage # Coverage
# Run single test file
cd apps/api && pnpm test -- auth.service.spec
cd apps/web && pnpm exec vitest src/services/__tests__/teamApi.test.ts
jest.mock('bcrypt') is at module level (before imports)as any for mock return valuesglobals: true to vitest.config.ts__tests__/ → ../Component)getAllByText or queryAllByText instead of singular variantsvi.hoisted() wraps mock variables, and vi.mock() path matches import exactlySwagger/OpenAPI setup and documentation patterns for SE104_VLEAGUE
Complete guide for the SE104_VLEAGUE React frontend — pages, services, components, auth flow, i18n, and patterns
Complete guide for the SE104_VLEAGUE NestJS backend — modules, endpoints, Prisma schema, guards, interceptors, and patterns
JWT auth, OAuth, OTP verification, session management, RBAC for SE104_VLEAGUE
All business rules, state machines, scoring, regulations, and domain constraints for SE104_VLEAGUE
Prisma schema, migrations, seeding, and database operations for SE104_VLEAGUE