| name | Testing Strategy |
| description | Test naming conventions, fixture patterns, mocking strategies, and coverage thresholds by stage |
Testing Strategy Skill
Covers unit, integration, and e2e test patterns for all supported frameworks.
Test File Organization
Folder Structure
- Unit tests: co-locate with source files
src/services/user.ts → src/services/__tests__/user.test.ts (Jest/Vitest)
src/services/user.ts → src/services/test_user.py (pytest)
- Integration tests: separate
tests/ directory at project root
tests/integration/auth.test.ts
tests/integration/database.test.ts
- E2E tests: separate
e2e/ or tests/e2e/ directory
e2e/flows/login.test.ts
e2e/flows/checkout.test.ts
File Naming Convention
- Unit:
<module>.test.ts or <module>_test.py
- Integration:
<feature>.integration.test.ts or test_<feature>.py
- E2E:
<flow>.e2e.test.ts or test_<flow>_e2e.py
Test Structure: Arrange-Act-Assert
All tests MUST follow AAA pattern:
describe('UserService', () => {
describe('createUser', () => {
it('should create a user with valid email and name', () => {
const email = 'alice@example.com';
const name = 'Alice Chen';
const user = service.createUser(email, name);
expect(user.email).toBe(email);
expect(user.name).toBe(name);
expect(user.id).toBeDefined();
});
});
});
Golden rule: One logical assertion per test (one reason to fail).
Test Naming: Descriptive, Not Clever
❌ BAD:
it('works', () => { ... });
it('user creation 1', () => { ... });
✅ GOOD:
it('should create a user with valid email and return user object with id', () => { ... });
it('should reject email without @ symbol', () => { ... });
it('should hash password before storing in database', () => { ... });
Pattern: should [what happens] [given conditions if not obvious]
Framework-Specific Setup
Node.js / TypeScript (Jest / Vitest)
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/index.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
{
"extends": "./tsconfig.json",
"compilerOptions": {
"jsx": "react",
"types": ["jest", "node"]
}
}
Python (pytest)
[pytest]
testpaths = tests
python_files = test_*.py *_test.py
python_classes = Test*
python_functions = test_*
addopts = --verbose --strict-markers
markers =
unit: unit tests
integration: integration tests
slow: slow running tests
import pytest
@pytest.fixture
def sample_user():
return {'id': 1, 'email': 'test@example.com', 'name': 'Test User'}
Go (standard testing)
package user
import "testing"
func TestCreateUser(t *testing.T) {
user := CreateUser("alice@example.com", "Alice")
if user.Email != "alice@example.com" {
t.Errorf("expected %q, got %q", "alice@example.com", user.Email)
}
}
.NET (xUnit)
public class UserServiceTests
{
[Fact]
public void CreateUser_WithValidEmail_ReturnsUserWithId()
{
var service = new UserService();
var user = service.CreateUser("alice@example.com", "Alice");
Assert.NotNull(user.Id);
Assert.Equal("alice@example.com", user.Email);
}
}
Mocking Strategy
Database Mocks (Unit Tests)
Use in-memory database for unit tests when possible:
module.exports = {
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts']
};
import { PrismaClient } from '@prisma/client';
beforeAll(async () => {
process.env.DATABASE_URL = 'file:./test.db';
await prisma.$executeRawUnsafe('PRAGMA foreign_keys = OFF');
});
afterEach(async () => {
await prisma.$queryRaw`DELETE FROM "User"`;
await prisma.$queryRaw`DELETE FROM "Post"`;
});
afterAll(async () => {
await prisma.$disconnect();
});
External Service Mocks (Unit Tests)
Mock HTTP calls, external APIs, payment gateways:
jest.mock('../lib/stripe', () => ({
createPayment: jest.fn().mockResolvedValue({ id: 'pay_123', status: 'succeeded' })
}));
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: [] })
})
);
Real Database (Integration Tests)
Use actual test database for integration tests:
beforeAll(async () => {
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5433/test_db';
await prisma.$executeRawUnsafe('CREATE SCHEMA IF NOT EXISTS tests');
});
afterAll(async () => {
await prisma.$executeRawUnsafe('DROP SCHEMA tests CASCADE');
});
Rule: If a test needs the database to run, it's an integration test, not a unit test. Don't mock the database for integration tests.
Fixture Patterns
Shared Fixtures (Reusable Test Data)
export const validUser = {
email: 'alice@example.com',
name: 'Alice Chen',
password: 'SecurePass123!'
};
export const adminUser = {
...validUser,
role: 'admin'
};
export const inactiveUser = {
...validUser,
status: 'inactive'
};
import { validUser, adminUser } from './fixtures/user.fixtures';
it('should allow admin to create users', () => {
const created = service.createUser(validUser, { createdBy: adminUser });
expect(created.createdBy).toBe(adminUser.id);
});
Factory Pattern (Dynamic Test Data)
class UserFactory {
static async create(overrides = {}) {
const defaults = {
email: `user-${Date.now()}@example.com`,
name: 'Test User',
role: 'user'
};
return prisma.user.create({
data: { ...defaults, ...overrides }
});
}
static async createMany(count, overrides = {}) {
return Promise.all(
Array.from({ length: count }).map(() => this.create(overrides))
);
}
}
const users = await UserFactory.createMany(10, { role: 'admin' });
expect(users).toHaveLength(10);
Coverage Thresholds by Stage
MVP Stage
- Unit tests: ≥ 70% coverage (critical paths only)
- Integration tests: ≥ 5 per service (authentication, CRUD, error cases)
- E2E tests: ≥ 2 per major flow (happy path + error case)
- Requirement: Tests must run in CI before merge
Growth Stage
- Unit tests: ≥ 80% coverage (all public APIs)
- Integration tests: ≥ 10 per service (edge cases, race conditions)
- E2E tests: ≥ 5 per major flow (happy path, errors, edge cases)
- Requirement: Failed tests block merge, coverage reports published
Enterprise Stage
- Unit tests: ≥ 85% coverage (all code paths including errors)
- Integration tests: ≥ 15 per service + contract tests
- E2E tests: ≥ 10 per major flow + performance tests
- Requirement: Coverage must improve or stay same; no regression allowed
Test Execution
NPM Script Convention
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:unit": "jest --testPathPattern=__tests__",
"test:integration": "jest --testPathPattern=integration",
"test:e2e": "playwright test"
}
}
Python Convention
pytest
pytest -m unit
pytest -m integration
pytest -v --cov=src
Go Convention
go test ./...
go test -cover ./...
go test -run TestCreateUser
Async / Await Testing
TypeScript (Jest)
it('should fetch user by id', async () => {
const user = await service.getUserById(1);
expect(user.id).toBe(1);
});
it('should reject invalid id', async () => {
await expect(service.getUserById(-1)).rejects.toThrow('Invalid ID');
});
Python (pytest)
import pytest
@pytest.mark.asyncio
async def test_fetch_user():
user = await service.get_user(1)
assert user.id == 1
from asyncio import run
def test_sync_wrapper():
user = run(service.get_user(1))
assert user.id == 1
Skip & Mark Patterns
Skip Flaky Tests
it.skip('should handle network timeout', () => {
});
@pytest.mark.skip(reason="flaky, TODO: fix race condition")
def test_concurrent_updates():
pass
Mark Slow Tests
module.exports = {
testTimeout: 30000
};
jest.setTimeout(5000);
Mark as WIP (Work in Progress)
it.todo('should support bulk user import');
CI/CD Integration
GitHub Actions
- name: Run Tests
run: npm test -- --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
Fail on Coverage Drop
- name: Check Coverage
run: |
COVERAGE=$(npm test -- --coverage | grep Lines | awk '{print $NF}' | sed 's/%//')
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "Coverage $COVERAGE% below threshold 80%"
exit 1
fi
Best Practices Checklist
- ✓ Each test file tests ONE class/module
- ✓ Each test method tests ONE behavior
- ✓ Test names describe the BEHAVIOR, not the method
- ✓ AAA structure (Arrange, Act, Assert)
- ✓ No test interdependencies (tests can run in any order)
- ✓ Mock external dependencies (APIs, databases, file system)
- ✓ Use real database for integration tests only
- ✓ Clean up after each test (teardown)
- ✓ Fixtures for reusable test data
- ✓ Factories for dynamic test data generation
- ✓ Timeouts for async tests
- ✓ Descriptive assertion messages
- ✓ One assertion per test when possible (one reason to fail)
- ✓ Avoid testing private implementation details
- ✓ Test behavior from the outside (black box)