Skip to main content

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

  1. Test behavior, not implementation
  2. One assertion per test (when possible)
  3. Use descriptive test names
  4. Keep tests independent
  5. Clean up after tests
  6. Don't test framework code
  7. Mock external dependencies