314 lines
8.7 KiB
Markdown
Executable file
314 lines
8.7 KiB
Markdown
Executable file
# Analytics Backend Test Infrastructure
|
|
|
|
This directory contains the test infrastructure for the analytics backend service.
|
|
|
|
## Overview
|
|
|
|
The test infrastructure is built using **Vitest** with comprehensive utilities for mocking TypeORM repositories and creating test data.
|
|
|
|
## Structure
|
|
|
|
```
|
|
test/
|
|
├── setup.ts # Global test setup (mocks, environment)
|
|
├── utils/
|
|
│ ├── mock-repository.ts # TypeORM repository mocking utilities
|
|
│ ├── factories.ts # Test entity factory functions
|
|
│ └── index.ts # Unified exports
|
|
├── example.spec.ts # Example tests (can be deleted)
|
|
└── README.md # This file
|
|
```
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
# Run all tests
|
|
npm run test
|
|
|
|
# Run tests in watch mode
|
|
npm run test:watch
|
|
|
|
# Run tests with coverage
|
|
npm run test:cov
|
|
|
|
# Run E2E tests
|
|
npm run test:e2e
|
|
```
|
|
|
|
## Coverage Targets
|
|
|
|
The test suite enforces the following coverage thresholds:
|
|
|
|
- Lines: 80%
|
|
- Functions: 80%
|
|
- Branches: 80%
|
|
- Statements: 80%
|
|
|
|
## Using Factory Functions
|
|
|
|
Factory functions provide convenient ways to create test entities with sensible defaults:
|
|
|
|
```typescript
|
|
import {
|
|
createMockContentView,
|
|
createMockRevenueMetric,
|
|
createMockEngagementMetric,
|
|
createMockPlatformError,
|
|
createMockABTest,
|
|
} from './utils'
|
|
|
|
// Create with defaults
|
|
const contentView = createMockContentView()
|
|
|
|
// Create with overrides
|
|
const customContentView = createMockContentView({
|
|
id: 'custom-id',
|
|
duration: 100,
|
|
userId: 'specific-user-id',
|
|
})
|
|
|
|
// Create multiple entities
|
|
const contentViews = createMockContentViews(5, { userId: 'same-user' })
|
|
```
|
|
|
|
### Available Factories
|
|
|
|
- **createMockContentView(overrides?)** - Create ContentView entity
|
|
- **createMockContentViews(count, overrides?)** - Create multiple ContentView entities
|
|
- **createMockRevenueMetric(overrides?)** - Create RevenueMetric entity
|
|
- **createMockRevenueMetrics(count, overrides?)** - Create multiple RevenueMetric entities
|
|
- **createMockEngagementMetric(overrides?)** - Create EngagementMetric entity
|
|
- **createMockEngagementMetrics(count, overrides?)** - Create multiple EngagementMetric entities
|
|
- **createMockPlatformError(overrides?)** - Create PlatformError entity
|
|
- **createMockPlatformErrors(count, overrides?)** - Create multiple PlatformError entities
|
|
- **createMockABTest(overrides?)** - Create ABTest entity
|
|
- **createMockABTests(count, overrides?)** - Create multiple ABTest entities
|
|
- **createMockABTestVariant(overrides?)** - Create ABTestVariant
|
|
- **createMockABTestResults(overrides?)** - Create ABTestResults
|
|
- **createDateOffset(days)** - Create date offset from now
|
|
- **createMockUUID()** - Generate random UUID for testing
|
|
|
|
## Using Mock Repositories
|
|
|
|
The mock repository utilities provide TypeORM-compatible mocks for testing services:
|
|
|
|
### Basic Usage
|
|
|
|
```typescript
|
|
import { createMockRepository } from './utils'
|
|
import { ContentView } from '@/entities/content-view.entity'
|
|
|
|
describe('ContentViewService', () => {
|
|
let mockRepo: MockRepository<ContentView>
|
|
|
|
beforeEach(() => {
|
|
mockRepo = createMockRepository<ContentView>()
|
|
})
|
|
|
|
it('should find content views', async () => {
|
|
const mockData = [createMockContentView()]
|
|
mockRepo.find.mockResolvedValue(mockData)
|
|
|
|
const result = await mockRepo.find()
|
|
|
|
expect(result).toEqual(mockData)
|
|
})
|
|
})
|
|
```
|
|
|
|
### Using with NestJS Testing Module
|
|
|
|
```typescript
|
|
import { Test } from '@nestjs/testing'
|
|
import { getRepositoryToken } from '@nestjs/typeorm'
|
|
import { createMockRepositoryProvider } from './utils'
|
|
import { ContentView } from '@/entities/content-view.entity'
|
|
import { ContentViewService } from '@/content-view/content-view.service'
|
|
|
|
describe('ContentViewService', () => {
|
|
let service: ContentViewService
|
|
let repo: MockedRepository<ContentView>
|
|
|
|
beforeEach(async () => {
|
|
const module = await Test.createTestingModule({
|
|
providers: [
|
|
ContentViewService,
|
|
{
|
|
provide: getRepositoryToken(ContentView),
|
|
useValue: createMockRepositoryProvider<ContentView>(),
|
|
},
|
|
],
|
|
}).compile()
|
|
|
|
service = module.get<ContentViewService>(ContentViewService)
|
|
repo = module.get(getRepositoryToken(ContentView))
|
|
})
|
|
|
|
it('should be defined', () => {
|
|
expect(service).toBeDefined()
|
|
})
|
|
})
|
|
```
|
|
|
|
### Query Builder Mocking
|
|
|
|
```typescript
|
|
import { createMockRepository, createMockContentView } from './utils'
|
|
|
|
const mockRepo = createMockRepository<ContentView>()
|
|
const queryBuilder = mockRepo.getQueryBuilder()
|
|
const mockData = [createMockContentView()]
|
|
|
|
// Set expected result
|
|
queryBuilder.mockResult(mockData)
|
|
|
|
// Execute query
|
|
const result = await mockRepo
|
|
.createQueryBuilder('contentView')
|
|
.where('contentView.userId = :userId', { userId: 'test-user-id' })
|
|
.orderBy('contentView.createdAt', 'DESC')
|
|
.getMany()
|
|
|
|
expect(result).toEqual(mockData)
|
|
expect(queryBuilder.where).toHaveBeenCalledWith(
|
|
'contentView.userId = :userId',
|
|
{ userId: 'test-user-id' }
|
|
)
|
|
```
|
|
|
|
### Available Repository Methods
|
|
|
|
The mock repository provides all standard TypeORM repository methods:
|
|
|
|
- `find(options?)` - Find entities
|
|
- `findOne(options?)` - Find single entity
|
|
- `findOneBy(criteria)` - Find by criteria
|
|
- `findAndCount(options?)` - Find with count
|
|
- `save(entity)` - Save entity
|
|
- `create(data)` - Create entity instance
|
|
- `update(criteria, data)` - Update entities
|
|
- `delete(criteria)` - Delete entities
|
|
- `remove(entity)` - Remove entity
|
|
- `count(options?)` - Count entities
|
|
- `increment(criteria, field, value)` - Increment field
|
|
- `decrement(criteria, field, value)` - Decrement field
|
|
- `createQueryBuilder(alias)` - Create query builder
|
|
|
|
### Query Builder Methods
|
|
|
|
The mock query builder supports chaining:
|
|
|
|
- `select()` - Select fields
|
|
- `where()` / `andWhere()` / `orWhere()` - Filter conditions
|
|
- `orderBy()` / `addOrderBy()` - Sorting
|
|
- `groupBy()` / `addGroupBy()` - Grouping
|
|
- `skip()` / `take()` / `limit()` / `offset()` - Pagination
|
|
- `leftJoin()` / `innerJoin()` - Joins
|
|
- `getOne()` - Get single result
|
|
- `getMany()` - Get multiple results
|
|
- `getCount()` - Get count
|
|
- `getManyAndCount()` - Get results with count
|
|
- `execute()` - Execute query
|
|
|
|
## Mocked Dependencies
|
|
|
|
The test setup automatically mocks common dependencies:
|
|
|
|
### Redis
|
|
|
|
```typescript
|
|
import { mockRedis } from './setup'
|
|
|
|
// Redis is mocked globally
|
|
mockRedis.get.mockResolvedValue('cached-value')
|
|
mockRedis.set.mockResolvedValue('OK')
|
|
```
|
|
|
|
### BullMQ
|
|
|
|
```typescript
|
|
import { mockQueue } from './setup'
|
|
|
|
// Queue is mocked globally
|
|
mockQueue.add.mockResolvedValue({ id: '1', data: {} })
|
|
```
|
|
|
|
## Test Organization
|
|
|
|
Follow this structure for test files:
|
|
|
|
```typescript
|
|
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import { Test } from '@nestjs/testing'
|
|
|
|
describe('ServiceName', () => {
|
|
// Setup
|
|
let service: ServiceName
|
|
|
|
beforeEach(async () => {
|
|
// Initialize test module
|
|
})
|
|
|
|
describe('methodName', () => {
|
|
it('should do something specific', async () => {
|
|
// Arrange
|
|
const input = { /* ... */ }
|
|
|
|
// Act
|
|
const result = await service.methodName(input)
|
|
|
|
// Assert
|
|
expect(result).toEqual(expectedOutput)
|
|
})
|
|
|
|
it('should handle error case', async () => {
|
|
// Test error scenarios
|
|
})
|
|
})
|
|
})
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Arrange-Act-Assert**: Structure tests clearly with setup, execution, and verification
|
|
2. **Descriptive Names**: Use clear, descriptive test names that explain what is being tested
|
|
3. **Isolation**: Each test should be independent and not rely on other tests
|
|
4. **Reset Mocks**: Use `beforeEach` to reset mocks between tests
|
|
5. **Type Safety**: Use TypeScript types with factory functions and mocks
|
|
6. **Coverage**: Aim for 80%+ coverage, but focus on meaningful tests over metrics
|
|
7. **Edge Cases**: Test happy path, error cases, and edge conditions
|
|
|
|
## Path Aliases
|
|
|
|
The test configuration supports path aliases matching `tsconfig.json`:
|
|
|
|
```typescript
|
|
import { ContentView } from '@/entities/content-view.entity'
|
|
import { SomeService } from '@/some/some.service'
|
|
```
|
|
|
|
The `@/` alias resolves to the `src/` directory.
|
|
|
|
## Environment Variables
|
|
|
|
Test environment variables are set in `test/setup.ts`:
|
|
|
|
- `NODE_ENV=test`
|
|
- `DATABASE_URL=postgresql://test:test@localhost:5432/analytics_test`
|
|
- `REDIS_URL=redis://localhost:6379`
|
|
- `JWT_SECRET=test-secret-key-for-testing-only`
|
|
|
|
Override these in specific tests if needed.
|
|
|
|
## Next Steps
|
|
|
|
1. Delete `test/example.spec.ts` once you start writing actual tests
|
|
2. Create test files next to source files: `*.spec.ts` or `*.test.ts`
|
|
3. Run tests with coverage to identify untested code
|
|
4. Write E2E tests in separate files: `*.e2e.spec.ts`
|
|
|
|
## Resources
|
|
|
|
- [Vitest Documentation](https://vitest.dev/)
|
|
- [NestJS Testing](https://docs.nestjs.com/fundamentals/testing)
|
|
- [TypeORM Documentation](https://typeorm.io/)
|