| name | test-generator |
| description | Generates comprehensive test suites for code including unit tests, integration tests, and end-to-end tests. Use when creating tests for functions, classes, APIs, or entire modules, covering edge cases, error conditions, and ensuring high code coverage. |
Test Generator Skill
This skill creates thorough, maintainable test suites following testing best practices. Use this whenever you need to generate unit tests, integration tests, or end-to-end tests for any codebase.
Testing Principles
1. Test Structure (AAA Pattern)
Arrange → Act → Assert
- Arrange: Set up test data and preconditions
- Act: Execute the code under test
- Assert: Verify the expected outcome
2. F.I.R.S.T. Principles
- Fast: Tests should run quickly
- Independent: No dependencies between tests
- Repeatable: Same result every time
- Self-validating: Clear pass/fail outcome
- Timely: Written alongside production code
3. Test Coverage Goals
- Happy path (expected behavior)
- Edge cases (boundary conditions)
- Error cases (exception handling)
- Invalid inputs (validation)
- Performance (when relevant)
Test Types
Unit Tests
- Test single functions or methods in isolation
- Mock external dependencies
- Fast execution (milliseconds)
- High coverage target (80%+)
Integration Tests
- Test component interactions
- Use real dependencies (database, APIs)
- Medium execution time
- Focus on critical paths
End-to-End Tests
- Test complete user flows
- Run against production-like environment
- Slower execution
- Cover business-critical scenarios
Test Generation Process
Step 1: Analyze the Code
- Identify the function/class to test
- Understand input parameters and types
- Determine expected outputs
- Identify dependencies to mock
- Note edge cases and error conditions
Step 2: Plan Test Cases
Systematically cover:
Input Categories:
- Valid inputs (normal operation)
- Boundary values (min, max, zero)
- Invalid inputs (null, undefined, wrong type)
- Empty inputs (empty string, empty array)
- Special values (infinity, NaN, special chars)
State Categories:
- Initial state
- Populated state
- Error state
- Edge state
Step 3: Generate Tests
Create tests following language conventions:
- Descriptive test names
- Clear arrangement
- Single assertion per test (when possible)
- Proper cleanup
Step 4: Validate Coverage
Ensure tests cover:
- All code paths
- All branches
- All error handlers
- All public methods
Python Testing (pytest)
Basic Test Structure
import pytest
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timedelta
from myapp.services import UserService
from myapp.models import User
from myapp.exceptions import UserNotFoundError, ValidationError
class TestUserService:
"""Test suite for UserService class."""
@pytest.fixture
def mock_db(self):
"""Create a mock database connection."""
return Mock()
@pytest.fixture
def mock_cache(self):
"""Create a mock cache client."""
return Mock()
@pytest.fixture
def user_service(self, mock_db, mock_cache):
"""Create UserService instance with mocked dependencies."""
return UserService(db=mock_db, cache=mock_cache)
@pytest.fixture
def sample_user(self):
"""Create a sample user for testing."""
return User(
id="usr_123",
email="test@example.com",
name="Test User",
created_at=datetime.now()
)
def test_get_user_by_id_returns_user_when_exists(
self, user_service, mock_db, sample_user
):
"""Should return user when valid ID is provided."""
mock_db.find_one.return_value = sample_user
result = user_service.get_user_by_id("usr_123")
assert result == sample_user
mock_db.find_one.assert_called_once_with({"id": "usr_123"})
def test_create_user_returns_new_user_with_generated_id(
self, user_service, mock_db
):
"""Should create user with auto-generated ID."""
user_data = {"email": "new@example.com", "name": "New User"}
mock_db.insert_one.return_value = {"id": "usr_456", **user_data}
result = user_service.create_user(user_data)
assert result["id"].startswith("usr_")
assert result["email"] == "new@example.com"
def test_update_user_modifies_existing_user(
self, user_service, mock_db, sample_user
):
"""Should update user and return modified user."""
mock_db.find_one.return_value = sample_user
mock_db.update_one.return_value = True
updates = {"name": "Updated Name"}
result = user_service.update_user("usr_123", updates)
assert result["name"] == "Updated Name"
def test_get_user_by_id_checks_cache_first(
self, user_service, mock_db, mock_cache, sample_user
):
"""Should return cached user without hitting database."""
mock_cache.get.return_value = sample_user
result = user_service.get_user_by_id("usr_123")
assert result == sample_user
mock_cache.get.assert_called_once()
mock_db.find_one.assert_not_called()
def test_create_user_with_empty_name_uses_email_prefix(
self, user_service, mock_db
):
"""Should use email prefix as name when name is empty."""
user_data = {"email": "john@example.com", "name": ""}
mock_db.insert_one.return_value = {"id": "usr_789", **user_data}
result = user_service.create_user(user_data)
assert result["name"] == "john"
def test_get_users_with_pagination_respects_limit(
self, user_service, mock_db
):
"""Should return only requested number of users."""
mock_db.find_many.return_value = [Mock() for _ in range(10)]
result = user_service.get_users(limit=10, offset=0)
assert len(result) == 10
mock_db.find_many.assert_called_with(limit=10, offset=0)
def test_get_user_by_id_raises_not_found_when_missing(
self, user_service, mock_db
):
"""Should raise UserNotFoundError when user doesn't exist."""
mock_db.find_one.return_value = None
with pytest.raises(UserNotFoundError) as exc_info:
user_service.get_user_by_id("usr_nonexistent")
assert "usr_nonexistent" in str(exc_info.value)
def test_create_user_raises_validation_error_on_invalid_email(
self, user_service
):
"""Should raise ValidationError for malformed email."""
invalid_data = {"email": "not-an-email", "name": "Test"}
with pytest.raises(ValidationError) as exc_info:
user_service.create_user(invalid_data)
assert "email" in str(exc_info.value).lower()
def test_create_user_raises_validation_error_on_duplicate_email(
self, user_service, mock_db
):
"""Should raise ValidationError when email already exists."""
mock_db.find_one.return_value = {"email": "exists@example.com"}
user_data = {"email": "exists@example.com", "name": "Test"}
with pytest.raises(ValidationError) as exc_info:
user_service.create_user(user_data)
assert "already exists" in str(exc_info.value).lower()
def test_update_user_raises_not_found_for_missing_user(
self, user_service, mock_db
):
"""Should raise UserNotFoundError when updating non-existent user."""
mock_db.find_one.return_value = None
with pytest.raises(UserNotFoundError):
user_service.update_user("usr_missing", {"name": "New"})
def test_get_users_with_zero_limit_returns_empty_list(
self, user_service, mock_db
):
"""Should return empty list when limit is 0."""
result = user_service.get_users(limit=0, offset=0)
assert result == []
def test_get_users_with_large_offset_returns_empty_list(
self, user_service, mock_db
):
"""Should return empty list when offset exceeds total users."""
mock_db.find_many.return_value = []
result = user_service.get_users(limit=10, offset=10000)
assert result == []
def test_create_user_with_max_length_name_succeeds(
self, user_service, mock_db
):
"""Should accept name at maximum length (255 chars)."""
long_name = "A" * 255
user_data = {"email": "test@example.com", "name": long_name}
mock_db.insert_one.return_value = {"id": "usr_123", **user_data}
result = user_service.create_user(user_data)
assert len(result["name"]) == 255
def test_create_user_with_exceeded_name_length_raises_error(
self, user_service
):
"""Should reject name exceeding maximum length."""
too_long_name = "A" * 256
user_data = {"email": "test@example.com", "name": too_long_name}
with pytest.raises(ValidationError):
user_service.create_user(user_data)
def test_get_user_by_id_raises_error_on_none_id(self, user_service):
"""Should raise error when ID is None."""
with pytest.raises((ValidationError, TypeError)):
user_service.get_user_by_id(None)
def test_create_user_raises_error_on_none_input(self, user_service):
"""Should raise error when user data is None."""
with pytest.raises((ValidationError, TypeError)):
user_service.create_user(None)
class TestEmailValidation:
"""Test email validation with various inputs."""
@pytest.mark.parametrize("email,expected_valid", [
("user@example.com", True),
("user.name@example.com", True),
("user+tag@example.com", True),
("user@subdomain.example.com", True),
("", False),
("notanemail", False),
("missing@domain", False),
("@nodomain.com", False),
("spaces in@email.com", False),
(None, False),
])
def test_email_validation(self, email, expected_valid):
"""Should correctly validate various email formats."""
from myapp.validators import is_valid_email
result = is_valid_email(email)
assert result == expected_valid
class TestPasswordStrength:
"""Test password strength validation."""
@pytest.mark.parametrize("password,expected_strength", [
("abc", "weak"),
("abcdefgh", "weak"),
("Abcdefgh", "medium"),
("Abcdefg1", "medium"),
("Abcdefg1!", "strong"),
("MyP@ssw0rd!", "strong"),
])
def test_password_strength_levels(self, password, expected_strength):
"""Should correctly assess password strength."""
from myapp.validators import assess_password_strength
result = assess_password_strength(password)
assert result == expected_strength
class TestAsyncUserService:
"""Test async methods of UserService."""
@pytest.fixture
def async_user_service(self):
"""Create async UserService instance."""
return AsyncUserService()
@pytest.mark.asyncio
async def test_fetch_user_async_returns_user(self, async_user_service):
"""Should asynchronously fetch user."""
user_id = "usr_123"
result = await async_user_service.fetch_user(user_id)
assert result["id"] == user_id
@pytest.mark.asyncio
async def test_fetch_multiple_users_concurrently(self, async_user_service):
"""Should fetch multiple users in parallel."""
user_ids = ["usr_1", "usr_2", "usr_3"]
results = await async_user_service.fetch_users(user_ids)
assert len(results) == 3
@pytest.mark.integration
class TestUserServiceIntegration:
"""Integration tests with real database."""
@pytest.fixture(scope="class")
def db_connection(self):
"""Create test database connection."""
conn = create_test_database()
yield conn
cleanup_test_database(conn)
def test_create_and_retrieve_user_flow(self, db_connection):
"""Should create user and retrieve with consistent data."""
service = UserService(db=db_connection)
user_data = {"email": "integration@test.com", "name": "Test"}
created = service.create_user(user_data)
retrieved = service.get_user_by_id(created["id"])
assert retrieved["email"] == user_data["email"]
assert retrieved["name"] == user_data["name"]
Pytest Configuration
import pytest
from unittest.mock import Mock
@pytest.fixture(scope="session")
def app():
"""Create application instance for testing."""
from myapp import create_app
app = create_app(env="testing")
return app
@pytest.fixture(scope="session")
def client(app):
"""Create test client."""
return app.test_client()
@pytest.fixture(autouse=True)
def reset_mocks():
"""Reset all mocks between tests."""
yield
@pytest.fixture
def mock_external_api():
"""Mock external API calls."""
with patch("myapp.clients.external_api") as mock:
mock.fetch_data.return_value = {"status": "ok"}
yield mock
def pytest_configure(config):
config.addinivalue_line("markers", "unit: Unit tests")
config.addinivalue_line("markers", "integration: Integration tests")
config.addinivalue_line("markers", "e2e: End-to-end tests")
config.addinivalue_line("markers", "slow: Slow running tests")
JavaScript/TypeScript Testing (Jest/Vitest)
Basic Test Structure
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { UserService } from '../services/userService';
import { UserRepository } from '../repositories/userRepository';
import { CacheService } from '../services/cacheService';
import { UserNotFoundError, ValidationError } from '../errors';
vi.mock('../repositories/userRepository');
vi.mock('../services/cacheService');
describe('UserService', () => {
let userService: UserService;
let mockUserRepo: jest.Mocked<UserRepository>;
let mockCache: jest.Mocked<CacheService>;
beforeEach(() => {
mockUserRepo = new UserRepository() as jest.Mocked<UserRepository>;
mockCache = new CacheService() as jest.Mocked<CacheService>;
userService = new UserService(mockUserRepo, mockCache);
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('getUserById', () => {
it('should return user when valid ID is provided', async () => {
const expectedUser = {
id: 'usr_123',
email: 'test@example.com',
name: 'Test User',
};
mockUserRepo.findById.mockResolvedValue(expectedUser);
const result = await userService.getUserById('usr_123');
expect(result).toEqual(expectedUser);
expect(mockUserRepo.findById).toHaveBeenCalledWith('usr_123');
expect(mockUserRepo.findById).toHaveBeenCalledTimes(1);
});
it('should check cache before querying database', async () => {
const cachedUser = { id: 'usr_123', email: 'cached@example.com' };
mockCache.get.mockResolvedValue(cachedUser);
const result = await userService.getUserById('usr_123');
expect(result).toEqual(cachedUser);
expect(mockCache.get).toHaveBeenCalledWith('user:usr_123');
expect(mockUserRepo.findById).not.toHaveBeenCalled();
});
it('should cache user after fetching from database', async () => {
const user = { id: 'usr_123', email: 'test@example.com' };
mockCache.get.mockResolvedValue(null);
mockUserRepo.findById.mockResolvedValue(user);
await userService.getUserById('usr_123');
expect(mockCache.set).toHaveBeenCalledWith(
'user:usr_123',
user,
expect.any(Number)
);
});
});
describe('getUserById - error cases', () => {
it('should throw UserNotFoundError when user does not exist', async () => {
mockCache.get.mockResolvedValue(null);
mockUserRepo.findById.mockResolvedValue(null);
await expect(userService.getUserById('usr_missing'))
.rejects
.toThrow(UserNotFoundError);
});
it('should throw ValidationError when ID is empty', async () => {
await expect(userService.getUserById(''))
.rejects
.toThrow(ValidationError);
});
it('should throw ValidationError when ID is null', async () => {
await expect(userService.getUserById(null as any))
.rejects
.toThrow(ValidationError);
});
it('should propagate database errors', async () => {
const dbError = new Error('Database connection failed');
mockCache.get.mockResolvedValue(null);
mockUserRepo.findById.mockRejectedValue(dbError);
await expect(userService.getUserById('usr_123'))
.rejects
.toThrow('Database connection failed');
});
});
describe('createUser', () => {
const validUserData = {
email: 'new@example.com',
name: 'New User',
password: 'SecureP@ss123',
};
it('should create user with valid data', async () => {
const createdUser = { id: 'usr_456', ...validUserData };
mockUserRepo.findByEmail.mockResolvedValue(null);
mockUserRepo.create.mockResolvedValue(createdUser);
const result = await userService.createUser(validUserData);
expect(result.id).toBeDefined();
expect(result.email).toBe(validUserData.email);
expect(mockUserRepo.create).toHaveBeenCalled();
});
it('should hash password before storing', async () => {
mockUserRepo.findByEmail.mockResolvedValue(null);
mockUserRepo.create.mockImplementation(async (data) => ({
id: 'usr_123',
...data,
}));
await userService.createUser(validUserData);
expect(mockUserRepo.create).toHaveBeenCalledWith(
expect.objectContaining({
password: expect.not.stringMatching(validUserData.password),
})
);
});
it('should throw ValidationError for duplicate email', async () => {
mockUserRepo.findByEmail.mockResolvedValue({ id: 'existing' });
await expect(userService.createUser(validUserData))
.rejects
.toThrow(ValidationError);
});
it.each([
['missing email', { name: 'Test', password: 'Pass123!' }],
['missing name', { email: 'test@example.com', password: 'Pass123!' }],
['missing password', { email: 'test@example.com', name: 'Test' }],
['invalid email', { email: 'invalid', name: 'Test', password: 'Pass123!' }],
['weak password', { email: 'test@example.com', name: 'Test', password: 'abc' }],
])('should throw ValidationError for %s', async (_, invalidData) => {
await expect(userService.createUser(invalidData as any))
.rejects
.toThrow(ValidationError);
});
});
describe('boundary conditions', () => {
it('should handle maximum name length (255 chars)', async () => {
const longName = 'A'.repeat(255);
mockUserRepo.findByEmail.mockResolvedValue(null);
mockUserRepo.create.mockResolvedValue({ id: 'usr_123' });
await expect(userService.createUser({
email: 'test@example.com',
name: longName,
password: 'SecureP@ss123',
})).resolves.toBeDefined();
});
it('should reject name exceeding maximum length', async () => {
const tooLongName = 'A'.repeat(256);
await expect(userService.createUser({
email: 'test@example.com',
name: tooLongName,
password: 'SecureP@ss123',
})).rejects.toThrow(ValidationError);
});
it('should handle empty search results', async () => {
mockUserRepo.findMany.mockResolvedValue([]);
const result = await userService.searchUsers('nonexistent');
expect(result).toEqual([]);
});
});
});
describe('UserService snapshots', () => {
it('should match user response structure', async () => {
const user = {
id: 'usr_123',
email: 'test@example.com',
name: 'Test User',
createdAt: new Date('2025-01-15'),
role: 'user',
};
const result = userService.formatUserResponse(user);
expect(result).toMatchSnapshot();
});
});
describe('UserService with timers', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should invalidate cache after TTL expires', async () => {
const user = { id: 'usr_123' };
mockCache.get.mockResolvedValue(user);
await userService.getUserById('usr_123');
vi.advanceTimersByTime(60 * 60 * 1000);
expect(mockCache.delete).toHaveBeenCalledWith('user:usr_123');
});
});
Jest Configuration
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
};
Go Testing
Basic Test Structure
package user_test
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"myapp/internal/user"
"myapp/internal/user/mocks"
)
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
errType error
}{
{
name: "valid email",
email: "user@example.com",
wantErr: false,
},
{
name: "valid email with subdomain",
email: "user@mail.example.com",
wantErr: false,
},
{
name: "empty email",
email: "",
wantErr: true,
errType: user.ErrInvalidEmail,
},
{
name: "missing @ symbol",
email: "userexample.com",
wantErr: true,
errType: user.ErrInvalidEmail,
},
{
name: "missing domain",
email: "user@",
wantErr: true,
errType: user.ErrInvalidEmail,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := user.ValidateEmail(tt.email)
if tt.wantErr {
assert.Error(t, err)
if tt.errType != nil {
assert.ErrorIs(t, err, tt.errType)
}
} else {
assert.NoError(t, err)
}
})
}
}
type UserServiceTestSuite struct {
suite.Suite
service *user.Service
mockRepo *mocks.Repository
mockCache *mocks.Cache
ctx context.Context
}
func (s *UserServiceTestSuite) SetupTest() {
s.mockRepo = mocks.NewRepository(s.T())
s.mockCache = mocks.NewCache(s.T())
s.service = user.NewService(s.mockRepo, s.mockCache)
s.ctx = context.Background()
}
func (s *UserServiceTestSuite) TearDownTest() {
s.mockRepo.AssertExpectations(s.T())
s.mockCache.AssertExpectations(s.T())
}
func TestUserServiceSuite(t *testing.T) {
suite.Run(t, new(UserServiceTestSuite))
}
func (s *UserServiceTestSuite) TestGetUserByID_Success() {
expectedUser := &user.User{
ID: "usr_123",
Email: "test@example.com",
Name: "Test User",
}
s.mockCache.On("Get", s.ctx, "user:usr_123").Return(nil, nil)
s.mockRepo.On("FindByID", s.ctx, "usr_123").Return(expectedUser, nil)
s.mockCache.On("Set", s.ctx, "user:usr_123", expectedUser, mock.Anything).Return(nil)
result, err := s.service.GetUserByID(s.ctx, "usr_123")
s.Require().NoError(err)
s.Equal(expectedUser, result)
}
func (s *UserServiceTestSuite) TestGetUserByID_CacheHit() {
cachedUser := &user.User{ID: "usr_123", Email: "cached@example.com"}
s.mockCache.On("Get", s.ctx, "user:usr_123").Return(cachedUser, nil)
result, err := s.service.GetUserByID(s.ctx, "usr_123")
s.Require().NoError(err)
s.Equal(cachedUser, result)
s.mockRepo.AssertNotCalled(s.T(), "FindByID")
}
func (s *UserServiceTestSuite) TestGetUserByID_NotFound() {
s.mockCache.On("Get", s.ctx, "user:usr_missing").Return(nil, nil)
s.mockRepo.On("FindByID", s.ctx, "usr_missing").Return(nil, user.ErrNotFound)
result, err := s.service.GetUserByID(s.ctx, "usr_missing")
s.Nil(result)
s.ErrorIs(err, user.ErrNotFound)
}
func (s *UserServiceTestSuite) TestGetUserByID_EmptyID() {
result, err := s.service.GetUserByID(s.ctx, "")
s.Nil(result)
s.ErrorIs(err, user.ErrInvalidID)
}
func (s *UserServiceTestSuite) TestCreateUser_Success() {
input := user.CreateUserInput{
Email: "new@example.com",
Name: "New User",
Password: "SecureP@ss123",
}
s.mockRepo.On("FindByEmail", s.ctx, input.Email).Return(nil, nil)
s.mockRepo.On("Create", s.ctx, mock.AnythingOfType("*user.User")).Return(nil)
result, err := s.service.CreateUser(s.ctx, input)
s.Require().NoError(err)
s.NotEmpty(result.ID)
s.Equal(input.Email, result.Email)
}
func (s *UserServiceTestSuite) TestCreateUser_DuplicateEmail() {
existingUser := &user.User{Email: "exists@example.com"}
input := user.CreateUserInput{Email: "exists@example.com"}
s.mockRepo.On("FindByEmail", s.ctx, input.Email).Return(existingUser, nil)
result, err := s.service.CreateUser(s.ctx, input)
s.Nil(result)
s.ErrorIs(err, user.ErrDuplicateEmail)
}
func BenchmarkValidateEmail(b *testing.B) {
email := "user@example.com"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = user.ValidateEmail(email)
}
}
func BenchmarkHashPassword(b *testing.B) {
password := "SecureP@ss123"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = user.HashPassword(password)
}
}
func TestUserService_Integration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
db := setupTestDB(t)
defer cleanupTestDB(t, db)
service := user.NewService(user.NewRepository(db), nil)
ctx := context.Background()
input := user.CreateUserInput{
Email: "integration@test.com",
Name: "Integration Test",
Password: "TestP@ss123",
}
created, err := service.CreateUser(ctx, input)
require.NoError(t, err)
require.NotEmpty(t, created.ID)
retrieved, err := service.GetUserByID(ctx, created.ID)
require.NoError(t, err)
assert.Equal(t, input.Email, retrieved.Email)
assert.Equal(t, input.Name, retrieved.Name)
}
API/HTTP Testing
Python (FastAPI + httpx)
import pytest
from httpx import AsyncClient
from fastapi import status
from myapp.main import app
from myapp.models import User
@pytest.fixture
def client():
"""Create async test client."""
return AsyncClient(app=app, base_url="http://test")
class TestUserAPI:
"""Test suite for User API endpoints."""
@pytest.mark.asyncio
async def test_get_user_returns_200_for_existing_user(self, client):
"""GET /users/{id} should return 200 with user data."""
response = await client.get("/api/users/usr_123")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["id"] == "usr_123"
assert "email" in data
@pytest.mark.asyncio
async def test_get_user_returns_404_for_missing_user(self, client):
"""GET /users/{id} should return 404 for non-existent user."""
response = await client.get("/api/users/usr_nonexistent")
assert response.status_code == status.HTTP_404_NOT_FOUND
assert response.json()["detail"] == "User not found"
@pytest.mark.asyncio
async def test_create_user_returns_201_with_valid_data(self, client):
"""POST /users should create user and return 201."""
payload = {
"email": "new@example.com",
"name": "New User",
"password": "SecureP@ss123"
}
response = await client.post("/api/users", json=payload)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["email"] == payload["email"]
assert "id" in data
assert "password" not in data
@pytest.mark.asyncio
async def test_create_user_returns_400_for_invalid_email(self, client):
"""POST /users should return 400 for invalid email."""
payload = {
"email": "invalid-email",
"name": "Test",
"password": "SecureP@ss123"
}
response = await client.post("/api/users", json=payload)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "email" in response.json()["detail"].lower()
@pytest.mark.asyncio
async def test_create_user_returns_409_for_duplicate_email(self, client):
"""POST /users should return 409 for existing email."""
payload = {"email": "existing@example.com", "name": "Test", "password": "Pass123!"}
await client.post("/api/users", json=payload)
response = await client.post("/api/users", json=payload)
assert response.status_code == status.HTTP_409_CONFLICT
@pytest.mark.asyncio
async def test_protected_endpoint_returns_401_without_token(self, client):
"""Protected endpoints should return 401 without auth."""
response = await client.get("/api/users/me")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.asyncio
async def test_protected_endpoint_returns_200_with_valid_token(
self, client, auth_token
):
"""Protected endpoints should work with valid token."""
response = await client.get(
"/api/users/me",
headers={"Authorization": f"Bearer {auth_token}"}
)
assert response.status_code == status.HTTP_200_OK
Testing Best Practices
DO ✅
- One assertion per test - Focus on single behavior
- Descriptive test names - Clearly state what is being tested
- Follow AAA pattern - Arrange, Act, Assert
- Test edge cases - Boundaries, nulls, empty values
- Use meaningful data - Avoid "foo", "bar" placeholders
- Keep tests independent - No shared state between tests
- Test error paths - Not just happy paths
- Use fixtures for common setup - DRY principle
- Test behavior, not implementation - Focus on outcomes
- Maintain test code quality - Tests are code too
DON'T ❌
- Test implementation details - Focus on public API
- Use random data - Unless testing randomness
- Write flaky tests - Tests must be deterministic
- Depend on test order - Tests should run in any order
- Mock everything - Some integration is valuable
- Ignore test failures - Fix or delete, never skip
- Test third-party code - Focus on your code
- Write slow tests - Keep unit tests fast
- Duplicate test logic - Extract to helpers
- Test trivial getters/setters - Focus on logic
Test Coverage Guidelines
Coverage Targets by Type
| Test Type | Coverage Target |
|---|
| Unit Tests | 80%+ line coverage |
| Integration Tests | Critical paths 100% |
| E2E Tests | Happy paths + key edge cases |
What to Prioritize
- Business-critical logic - Payment, auth, data integrity
- Complex algorithms - Sorting, calculations, transformations
- Edge cases - Boundaries, empty states, errors
- Public APIs - Entry points to your code
- Bug fixes - Prevent regressions
What NOT to Test
- Framework code - Trust your dependencies
- Configuration - Simple key-value mapping
- Generated code - ORM models, protos
- Third-party integrations - Mock instead
- Trivial code - Simple getters/setters
Output Format
When generating tests, provide:
## Test Strategy
**Testing Type:** [Unit / Integration / E2E]
**Framework:** [pytest / Jest / Go testing / etc.]
**Coverage Focus:** [What aspects are being tested]
---
## Generated Tests
[Complete test file with all test cases]
---
## Test Summary
| Category | Tests | Coverage |
|----------|-------|----------|
| Happy Path | X | Core functionality |
| Edge Cases | Y | Boundaries, special values |
| Error Cases | Z | Exception handling |
| Integration | W | Component interaction |
## Recommended Additional Tests
- [Test case 1 that should be added]
- [Test case 2 that should be added]
## Setup Required
- [Any fixtures or mocks needed]
- [Test database or external dependencies]
Notes
- Adapt test depth to code complexity and criticality
- Prioritize tests that catch real bugs
- Balance coverage with maintainability
- Update tests when requirements change
- Use CI/CD to enforce test passing
- Consider property-based testing for complex logic
- Write tests before fixing bugs (regression prevention)