"🎉 Initial release: AI Proxy Worker v1.0"
This commit is contained in:
731
docs/Testing.en.md
Normal file
731
docs/Testing.en.md
Normal file
@@ -0,0 +1,731 @@
|
||||
# Testing Guide
|
||||
|
||||
<div align="center">
|
||||
|
||||
**🌍 Language / 语言**
|
||||
|
||||
[🇺🇸 English](./Testing.en.md) | [🇨🇳 中文](./Testing.md)
|
||||
|
||||
</div>
|
||||
|
||||
Comprehensive testing guide for AI Proxy Worker. This document covers testing strategies, tools, and best practices for ensuring code quality and reliability.
|
||||
|
||||
## 📋 Testing Overview
|
||||
|
||||
### Testing Philosophy
|
||||
- **Test Early, Test Often**: Write tests as you develop features
|
||||
- **Quality Over Quantity**: Focus on meaningful tests rather than coverage numbers
|
||||
- **Real-World Scenarios**: Test actual use cases and edge conditions
|
||||
- **Maintainable Tests**: Write tests that are easy to understand and maintain
|
||||
|
||||
### Testing Pyramid
|
||||
```
|
||||
/\
|
||||
/ \ Unit Tests (70%)
|
||||
/____\ - Fast, isolated, focused
|
||||
/ \
|
||||
/________\ Integration Tests (20%)
|
||||
- API endpoints, data flow
|
||||
|
||||
E2E Tests (10%)
|
||||
- Full user scenarios
|
||||
```
|
||||
|
||||
## 🧪 Testing Setup
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+ and npm
|
||||
- Wrangler CLI for Cloudflare Workers
|
||||
- Testing framework (Jest recommended)
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Install testing dependencies
|
||||
npm install --save-dev jest @types/jest
|
||||
npm install --save-dev @cloudflare/workers-types
|
||||
|
||||
# For API testing
|
||||
npm install --save-dev supertest
|
||||
npm install --save-dev node-fetch
|
||||
|
||||
# For mocking
|
||||
npm install --save-dev jest-environment-miniflare
|
||||
```
|
||||
|
||||
### Configuration
|
||||
Create `jest.config.js`:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
testEnvironment: 'miniflare',
|
||||
testEnvironmentOptions: {
|
||||
scriptPath: './worker.js',
|
||||
modules: true,
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setup.js'],
|
||||
collectCoverageFrom: [
|
||||
'worker.js',
|
||||
'!**/node_modules/**',
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Unit Testing
|
||||
|
||||
### Testing Worker Functions
|
||||
Test individual functions in isolation:
|
||||
|
||||
```javascript
|
||||
// tests/validation.test.js
|
||||
import { validateChatRequest } from '../worker.js'
|
||||
|
||||
describe('validateChatRequest', () => {
|
||||
it('should validate correct chat request', () => {
|
||||
const validRequest = {
|
||||
messages: [
|
||||
{ role: 'user', content: 'Hello, AI!' }
|
||||
],
|
||||
model: 'deepseek-chat'
|
||||
}
|
||||
|
||||
const result = validateChatRequest(validRequest)
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should reject request without messages', () => {
|
||||
const invalidRequest = {
|
||||
model: 'deepseek-chat'
|
||||
}
|
||||
|
||||
const result = validateChatRequest(invalidRequest)
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('messages must be an array')
|
||||
})
|
||||
|
||||
it('should reject invalid message format', () => {
|
||||
const invalidRequest = {
|
||||
messages: [
|
||||
{ role: 'invalid', content: 'Hello' }
|
||||
],
|
||||
model: 'deepseek-chat'
|
||||
}
|
||||
|
||||
const result = validateChatRequest(invalidRequest)
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('messages[0].role must be user, assistant, or system')
|
||||
})
|
||||
|
||||
it('should reject messages that are too long', () => {
|
||||
const longContent = 'a'.repeat(100001)
|
||||
const invalidRequest = {
|
||||
messages: [
|
||||
{ role: 'user', content: longContent }
|
||||
],
|
||||
model: 'deepseek-chat'
|
||||
}
|
||||
|
||||
const result = validateChatRequest(invalidRequest)
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('messages[0].content exceeds maximum length')
|
||||
})
|
||||
|
||||
it('should validate optional parameters', () => {
|
||||
const requestWithParams = {
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
model: 'deepseek-chat',
|
||||
temperature: 0.7,
|
||||
max_tokens: 1000
|
||||
}
|
||||
|
||||
const result = validateChatRequest(requestWithParams)
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject invalid temperature', () => {
|
||||
const invalidRequest = {
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
model: 'deepseek-chat',
|
||||
temperature: 3.0 // Invalid: > 2.0
|
||||
}
|
||||
|
||||
const result = validateChatRequest(invalidRequest)
|
||||
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors).toContain('temperature must be a number between 0 and 2')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Error Handling
|
||||
Test error scenarios thoroughly:
|
||||
|
||||
```javascript
|
||||
// tests/error-handling.test.js
|
||||
import { createErrorResponse, ApiError } from '../worker.js'
|
||||
|
||||
describe('Error Handling', () => {
|
||||
describe('createErrorResponse', () => {
|
||||
it('should create standard error response', () => {
|
||||
const error = new Error('Test error')
|
||||
const response = createErrorResponse(error, 400)
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
expect(response.headers.get('Content-Type')).toBe('application/json')
|
||||
})
|
||||
|
||||
it('should include error details in debug mode', () => {
|
||||
const error = new Error('Test error')
|
||||
const env = { DEBUG_MODE: 'true' }
|
||||
const details = { operation: 'test' }
|
||||
|
||||
const response = createErrorResponse(error, 400, details, env)
|
||||
const body = JSON.parse(response.body)
|
||||
|
||||
expect(body.details).toEqual(details)
|
||||
})
|
||||
|
||||
it('should not include details in production', () => {
|
||||
const error = new Error('Test error')
|
||||
const env = { DEBUG_MODE: 'false' }
|
||||
const details = { operation: 'test' }
|
||||
|
||||
const response = createErrorResponse(error, 400, details, env)
|
||||
const body = JSON.parse(response.body)
|
||||
|
||||
expect(body.details).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('ApiError', () => {
|
||||
it('should create API error with status code', () => {
|
||||
const apiError = new ApiError('Upstream API failed', 502)
|
||||
|
||||
expect(apiError.message).toBe('Upstream API failed')
|
||||
expect(apiError.statusCode).toBe(502)
|
||||
expect(apiError.name).toBe('ApiError')
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Utilities
|
||||
Test helper functions:
|
||||
|
||||
```javascript
|
||||
// tests/utils.test.js
|
||||
import { sanitizeForLogging, getClientIP } from '../worker.js'
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
describe('sanitizeForLogging', () => {
|
||||
it('should remove sensitive fields', () => {
|
||||
const data = {
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
api_key: 'sk-secret',
|
||||
authorization: 'Bearer token'
|
||||
}
|
||||
|
||||
const sanitized = sanitizeForLogging(data)
|
||||
|
||||
expect(sanitized.api_key).toBeUndefined()
|
||||
expect(sanitized.authorization).toBeUndefined()
|
||||
expect(sanitized.messages).toBeDefined()
|
||||
})
|
||||
|
||||
it('should truncate long content', () => {
|
||||
const longContent = 'a'.repeat(200)
|
||||
const data = {
|
||||
messages: [{ role: 'user', content: longContent }]
|
||||
}
|
||||
|
||||
const sanitized = sanitizeForLogging(data)
|
||||
|
||||
expect(sanitized.messages[0].content).toHaveLength(103) // 100 + "..."
|
||||
expect(sanitized.messages[0].content).toContain('...[truncated]')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getClientIP', () => {
|
||||
it('should extract IP from CF-Connecting-IP header', () => {
|
||||
const request = new Request('https://example.com', {
|
||||
headers: { 'CF-Connecting-IP': '192.168.1.1' }
|
||||
})
|
||||
|
||||
const ip = getClientIP(request)
|
||||
expect(ip).toBe('192.168.1.1')
|
||||
})
|
||||
|
||||
it('should fallback to X-Forwarded-For', () => {
|
||||
const request = new Request('https://example.com', {
|
||||
headers: { 'X-Forwarded-For': '192.168.1.2' }
|
||||
})
|
||||
|
||||
const ip = getClientIP(request)
|
||||
expect(ip).toBe('192.168.1.2')
|
||||
})
|
||||
|
||||
it('should return unknown for missing headers', () => {
|
||||
const request = new Request('https://example.com')
|
||||
|
||||
const ip = getClientIP(request)
|
||||
expect(ip).toBe('unknown')
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 🌐 Integration Testing
|
||||
|
||||
### Testing HTTP Endpoints
|
||||
Test complete request/response cycles:
|
||||
|
||||
```javascript
|
||||
// tests/integration/chat.test.js
|
||||
import { unstable_dev } from 'wrangler'
|
||||
|
||||
describe('Chat Endpoint Integration', () => {
|
||||
let worker
|
||||
|
||||
beforeAll(async () => {
|
||||
worker = await unstable_dev('worker.js', {
|
||||
experimental: { disableExperimentalWarning: true },
|
||||
env: {
|
||||
DEEPSEEK_API_KEY: 'test-key',
|
||||
PROXY_KEY: 'test-proxy-key'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await worker.stop()
|
||||
})
|
||||
|
||||
it('should handle valid chat request', async () => {
|
||||
const response = await worker.fetch('https://worker.dev/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-proxy-key',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [
|
||||
{ role: 'user', content: 'Hello, AI!' }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data.choices).toBeDefined()
|
||||
expect(data.choices[0].message).toBeDefined()
|
||||
})
|
||||
|
||||
it('should reject request without authorization', async () => {
|
||||
const response = await worker.fetch('https://worker.dev/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [{ role: 'user', content: 'Hello' }]
|
||||
})
|
||||
})
|
||||
|
||||
expect(response.status).toBe(401)
|
||||
})
|
||||
|
||||
it('should handle malformed JSON', async () => {
|
||||
const response = await worker.fetch('https://worker.dev/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-proxy-key',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: 'invalid json'
|
||||
})
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data.error).toBe('invalid_json')
|
||||
})
|
||||
|
||||
it('should handle streaming requests', async () => {
|
||||
const response = await worker.fetch('https://worker.dev/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-proxy-key',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [{ role: 'user', content: 'Hello' }],
|
||||
stream: true
|
||||
})
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('Content-Type')).toContain('text/event-stream')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Testing Rate Limiting
|
||||
Test rate limiting functionality:
|
||||
|
||||
```javascript
|
||||
// tests/integration/rate-limiting.test.js
|
||||
describe('Rate Limiting', () => {
|
||||
let worker
|
||||
|
||||
beforeAll(async () => {
|
||||
worker = await unstable_dev('worker.js', {
|
||||
env: {
|
||||
RATE_LIMIT_MAX_REQUESTS: '5',
|
||||
RATE_LIMIT_WINDOW_MS: '60000'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await worker.stop()
|
||||
})
|
||||
|
||||
it('should allow requests within limit', async () => {
|
||||
const requests = Array(3).fill().map(() =>
|
||||
worker.fetch('https://worker.dev/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-proxy-key',
|
||||
'Content-Type': 'application/json',
|
||||
'CF-Connecting-IP': '192.168.1.100'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [{ role: 'user', content: 'Hello' }]
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const responses = await Promise.all(requests)
|
||||
responses.forEach(response => {
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
it('should block requests exceeding limit', async () => {
|
||||
// Make requests up to the limit
|
||||
const requests = Array(6).fill().map(() =>
|
||||
worker.fetch('https://worker.dev/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-proxy-key',
|
||||
'Content-Type': 'application/json',
|
||||
'CF-Connecting-IP': '192.168.1.101'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [{ role: 'user', content: 'Hello' }]
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const responses = await Promise.all(requests)
|
||||
|
||||
// First 5 should succeed
|
||||
responses.slice(0, 5).forEach(response => {
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
|
||||
// 6th should be rate limited
|
||||
expect(responses[5].status).toBe(429)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 🎭 Mocking External APIs
|
||||
|
||||
### Mock DeepSeek API Responses
|
||||
Create mocks for external API calls:
|
||||
|
||||
```javascript
|
||||
// tests/mocks/deepseek-api.js
|
||||
export const mockDeepSeekResponse = {
|
||||
id: 'chatcmpl-test123',
|
||||
object: 'chat.completion',
|
||||
created: Date.now(),
|
||||
model: 'deepseek-chat',
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: 'Hello! How can I help you today?'
|
||||
},
|
||||
finish_reason: 'stop'
|
||||
}],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 9,
|
||||
total_tokens: 19
|
||||
}
|
||||
}
|
||||
|
||||
export const mockStreamingResponse = [
|
||||
'data: {"choices":[{"delta":{"content":"Hello"}}]}\n\n',
|
||||
'data: {"choices":[{"delta":{"content":"! How"}}]}\n\n',
|
||||
'data: {"choices":[{"delta":{"content":" can I help you?"}}]}\n\n',
|
||||
'data: [DONE]\n\n'
|
||||
]
|
||||
|
||||
// Mock fetch for testing
|
||||
export function mockFetch(url, options) {
|
||||
if (url.includes('api.deepseek.com')) {
|
||||
if (options.headers.Accept?.includes('text/event-stream')) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: new Map([['content-type', 'text/event-stream']]),
|
||||
body: {
|
||||
getReader: () => ({
|
||||
read: mockStreamingResponse.shift()
|
||||
? () => Promise.resolve({
|
||||
done: false,
|
||||
value: new TextEncoder().encode(mockStreamingResponse.shift())
|
||||
})
|
||||
: () => Promise.resolve({ done: true })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve(mockDeepSeekResponse)
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Unmocked URL'))
|
||||
}
|
||||
```
|
||||
|
||||
### Using Mocks in Tests
|
||||
```javascript
|
||||
// tests/integration/api-mocking.test.js
|
||||
import { mockFetch, mockDeepSeekResponse } from '../mocks/deepseek-api.js'
|
||||
|
||||
describe('API Integration with Mocks', () => {
|
||||
beforeEach(() => {
|
||||
global.fetch = jest.fn(mockFetch)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should handle successful API response', async () => {
|
||||
const worker = await unstable_dev('worker.js')
|
||||
|
||||
const response = await worker.fetch('https://worker.dev/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-key',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [{ role: 'user', content: 'Hello' }]
|
||||
})
|
||||
})
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data.choices[0].message.content).toBe(mockDeepSeekResponse.choices[0].message.content)
|
||||
|
||||
await worker.stop()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 🔍 Performance Testing
|
||||
|
||||
### Load Testing
|
||||
Test performance under load:
|
||||
|
||||
```javascript
|
||||
// tests/performance/load.test.js
|
||||
describe('Performance Tests', () => {
|
||||
it('should handle concurrent requests', async () => {
|
||||
const worker = await unstable_dev('worker.js')
|
||||
const concurrentRequests = 50
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
const requests = Array(concurrentRequests).fill().map(() =>
|
||||
worker.fetch('https://worker.dev/chat', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer test-key',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'deepseek-chat',
|
||||
messages: [{ role: 'user', content: 'Performance test' }]
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const responses = await Promise.all(requests)
|
||||
const endTime = Date.now()
|
||||
|
||||
// All requests should succeed
|
||||
responses.forEach(response => {
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
|
||||
// Should complete within reasonable time
|
||||
const duration = endTime - startTime
|
||||
expect(duration).toBeLessThan(10000) // 10 seconds
|
||||
|
||||
console.log(`${concurrentRequests} concurrent requests completed in ${duration}ms`)
|
||||
|
||||
await worker.stop()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 📊 Test Coverage
|
||||
|
||||
### Coverage Configuration
|
||||
Ensure good test coverage:
|
||||
|
||||
```javascript
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
collectCoverage: true,
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov', 'html'],
|
||||
collectCoverageFrom: [
|
||||
'worker.js',
|
||||
'src/**/*.js',
|
||||
'!**/node_modules/**',
|
||||
'!coverage/**',
|
||||
'!tests/**'
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 85,
|
||||
lines: 85,
|
||||
statements: 85
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Running Coverage
|
||||
```bash
|
||||
# Generate coverage report
|
||||
npm run test:coverage
|
||||
|
||||
# View HTML coverage report
|
||||
open coverage/lcov-report/index.html
|
||||
```
|
||||
|
||||
## 🚀 Continuous Integration
|
||||
|
||||
### GitHub Actions Workflow
|
||||
Create `.github/workflows/test.yml`:
|
||||
|
||||
```yaml
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run linting
|
||||
run: npm run lint
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:coverage
|
||||
env:
|
||||
DEEPSEEK_API_KEY: ${{ secrets.TEST_DEEPSEEK_API_KEY }}
|
||||
PROXY_KEY: ${{ secrets.TEST_PROXY_KEY }}
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
|
||||
- name: Deploy to test environment
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: npx wrangler publish --env test
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
```
|
||||
|
||||
## 📝 Testing Best Practices
|
||||
|
||||
### Test Structure
|
||||
- **AAA Pattern**: Arrange, Act, Assert
|
||||
- **Descriptive Names**: Test names should explain what they test
|
||||
- **Single Responsibility**: One test should test one thing
|
||||
- **Independent Tests**: Tests should not depend on each other
|
||||
|
||||
### Test Data
|
||||
- **Use Factories**: Create test data with factory functions
|
||||
- **Realistic Data**: Use data that resembles production data
|
||||
- **Edge Cases**: Test boundary conditions and edge cases
|
||||
|
||||
### Assertions
|
||||
- **Specific Assertions**: Use specific matchers for better error messages
|
||||
- **Multiple Assertions**: Group related assertions in the same test
|
||||
- **Error Testing**: Test both success and failure scenarios
|
||||
|
||||
### Maintenance
|
||||
- **Keep Tests Updated**: Update tests when code changes
|
||||
- **Remove Dead Tests**: Delete tests for removed functionality
|
||||
- **Refactor Tests**: Keep test code clean and maintainable
|
||||
|
||||
---
|
||||
|
||||
**Good tests are an investment in code quality** 🧪
|
||||
|
||||
Comprehensive testing ensures your AI Proxy Worker is reliable, maintainable, and ready for production use.
|
||||
Reference in New Issue
Block a user