Testing Strategy
CORTEX uses a comprehensive testing approach with multiple test levels.
Test Pyramid
┌─────────────────┐
│ E2E Tests │ Few, slow, high confidence
│ │
└────────┬────────┘
│
┌────────▼────────┐
│ Integration │ Some, medium speed
│ Tests │
└────────┬────────┘
│
┌────────▼────────┐
│ Unit Tests │ Many, fast, focused
│ │
└─────────────────┘
Test Types
Unit Tests
Test individual functions and classes in isolation.
// user.service.spec.ts
describe('UserService', () => {
let service: UserService;
let prisma: DeepMockProxy<PrismaClient>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UserService,
{
provide: PrismaService,
useValue: mockDeep<PrismaClient>(),
},
],
}).compile();
service = module.get(UserService);
prisma = module.get(PrismaService);
});
describe('findById', () => {
it('should return user when found', async () => {
const mockUser = { id: 'uuid', email: 'test@example.com' };
prisma.user.findUnique.mockResolvedValue(mockUser);
const result = await service.findById('uuid');
expect(result).toEqual(mockUser);
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { id: 'uuid' },
});
});
it('should throw NotFoundException when not found', async () => {
prisma.user.findUnique.mockResolvedValue(null);
await expect(service.findById('uuid')).rejects.toThrow(
NotFoundException,
);
});
});
});
Integration Tests
Test multiple components working together.
// auth.integration.spec.ts
describe('Auth Integration', () => {
let app: INestApplication;
let prisma: PrismaService;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = module.createNestApplication();
prisma = module.get(PrismaService);
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('POST /auth/login', () => {
it('should return tokens on valid credentials', async () => {
// Create test user
await prisma.user.create({
data: {
email: 'test@example.com',
passwordHash: await hash('password'),
// ...
},
});
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@example.com', password: 'password' });
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('accessToken');
expect(response.body).toHaveProperty('refreshToken');
});
});
});
Functional (E2E) Tests
Test complete user flows.
// F-AUTH-001.registration.func-spec.ts
describe('F-AUTH-001: User Registration', () => {
let app: INestApplication;
let testHelper: TestHelper;
beforeAll(async () => {
app = await setupTestApp();
testHelper = new TestHelper(app);
});
it('should complete full registration flow', async () => {
// Register
const registerResponse = await testHelper.register({
email: 'newuser@example.com',
password: 'SecureP@ss123',
firstName: 'New',
lastName: 'User',
});
expect(registerResponse.status).toBe(201);
expect(registerResponse.body.user.email).toBe('newuser@example.com');
// Login with new account
const loginResponse = await testHelper.login({
email: 'newuser@example.com',
password: 'SecureP@ss123',
});
expect(loginResponse.status).toBe(200);
// Access protected resource
const meResponse = await testHelper.getMe(loginResponse.body.accessToken);
expect(meResponse.status).toBe(200);
});
});
Test Organization
test/
├── unit/ # Unit tests (alongside source)
│ └── *.spec.ts
├── integration/ # Integration tests
│ └── *.integration.spec.ts
└── functional/ # E2E tests
├── suites/
│ ├── auth/
│ ├── tenant/
│ ├── user/
│ └── rbac/
├── helpers/ # Test utilities
└── fixtures/ # Test data
Running Tests
# All tests
pnpm test
# Unit tests only
pnpm test:unit
# Integration tests
pnpm test:integration
# Functional tests
pnpm test:func
# With coverage
pnpm test:cov
# Watch mode
pnpm test:watch
# Specific file
pnpm test -- user.service.spec.ts
Test Coverage
Requirements
- Minimum 80% line coverage
- Minimum 70% branch coverage
- Critical paths must have 100% coverage
Checking Coverage
pnpm test:cov
# Output
----------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
----------------------|---------|----------|---------|---------|
All files | 85.12 | 72.34 | 81.23 | 84.56 |
auth/ | 92.45 | 85.12 | 90.00 | 91.23 |
user/ | 78.34 | 65.43 | 75.00 | 77.89 |
Mocking
Mocking Services
const mockUserService = {
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
};
const module = await Test.createTestingModule({
providers: [
UserController,
{ provide: UserService, useValue: mockUserService },
],
}).compile();
Mocking Prisma
import { mockDeep, DeepMockProxy } from 'jest-mock-extended';
const mockPrisma = mockDeep<PrismaClient>();
mockPrisma.user.findUnique.mockResolvedValue(mockUser);
Test Data
Factories
// test/factories/user.factory.ts
export function createTestUser(overrides?: Partial<User>): User {
return {
id: 'test-user-id',
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
status: 'ACTIVE',
...overrides,
};
}
Fixtures
// test/fixtures/users.ts
export const testUsers = {
admin: createTestUser({ email: 'admin@example.com' }),
member: createTestUser({ email: 'member@example.com' }),
suspended: createTestUser({ status: 'SUSPENDED' }),
};
Best Practices
- Test behavior, not implementation
- One assertion per test (when possible)
- Use descriptive test names
- Keep tests independent
- Clean up after tests
- Don't test framework code
- Mock external dependencies