Unit Test Writing Guidelines
When writing unit tests, follow these core principles and guidelines.
Core Principles
1. No Duplicate Testing
Each test should verify a unique behavior. Avoid redundant test cases that exercise the same code path.
Bad: Testing a function that processes a list with separate tests for 1 element, 2 elements, and n elements when they all follow the same logic.
Good: Test the meaningful edge cases only - empty list, single element (if there's special handling), and a representative case for multiple elements.
2. Mock External Packages Only
Use mocks for external dependencies from outside the module. Do NOT mock:
- Core/standard library modules (typing, collections, etc.)
- Data classes and models (pydantic, dataclasses, attrs, TypedDict)
- Pure utility functions within the same package
Mock these:
- External API clients (requests, httpx, boto3)
- Database connections and ORMs
- File system operations (when testing logic, not I/O)
- Third-party service integrations
- Time/datetime for deterministic tests
3. Never Mock Private Methods
Private methods (_method in Python, #method in JS) should be exercised through their public interface, not mocked. If you need to mock a private method, it's a sign the code should be refactored.
Test File Location
Place test files alongside source files:
src/
user_service.py
test_user_service.py
utils/
helpers.py
test_helpers.py
Language-Specific Guidelines
Python (pytest)
Use pytest unless the project already uses unittest.
import pytest
from unittest.mock import Mock, patch, MagicMock
# Fixture for reusable setup
@pytest.fixture
def user_service(mock_db):
return UserService(db=mock_db)
# Mock external dependency
@pytest.fixture
def mock_db():
return Mock(spec=DatabaseClient)
# Test naming: test_<method>_<scenario>_<expected>
def test_create_user_valid_input_returns_user(user_service, mock_db):
# Arrange
mock_db.insert.return_value = {"id": 1, "name": "Alice"}
# Act
result = user_service.create_user("Alice")
# Assert
assert result.name == "Alice"
mock_db.insert.assert_called_once()
# Use parametrize for varying inputs with same logic
@pytest.mark.parametrize("invalid_name", ["", None, "x" * 256])
def test_create_user_invalid_name_raises(user_service, invalid_name):
with pytest.raises(ValidationError):
user_service.create_user(invalid_name)
Do NOT mock:
- Pydantic models - instantiate them directly
- Dataclasses - use real instances
- Pure functions in the same module
JavaScript/TypeScript (Jest/Vitest)
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from './user-service';
// Mock external module
vi.mock('./api-client', () => ({
ApiClient: vi.fn().mockImplementation(() => ({
post: vi.fn(),
})),
}));
describe('UserService', () => {
let service: UserService;
let mockApiClient: MockedObject<ApiClient>;
beforeEach(() => {
mockApiClient = new ApiClient() as MockedObject<ApiClient>;
service = new UserService(mockApiClient);
});
it('creates user with valid input', async () => {
mockApiClient.post.mockResolvedValue({ id: 1, name: 'Alice' });
const result = await service.createUser('Alice');
expect(result.name).toBe('Alice');
expect(mockApiClient.post).toHaveBeenCalledOnce();
});
});
Go (testing + testify)
package user
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Mock for external dependency
type MockDB struct {
mock.Mock
}
func (m *MockDB) Insert(data map[string]any) (map[string]any, error) {
args := m.Called(data)
return args.Get(0).(map[string]any), args.Error(1)
}
func TestCreateUser_ValidInput_ReturnsUser(t *testing.T) {
// Arrange
mockDB := new(MockDB)
mockDB.On("Insert", mock.Anything).Return(map[string]any{"id": 1, "name": "Alice"}, nil)
service := NewUserService(mockDB)
// Act
result, err := service.CreateUser("Alice")
// Assert
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
mockDB.AssertExpectations(t)
}
Test Structure (AAA Pattern)
Always organize tests with clear sections:
- Arrange - Set up test data and mocks
- Act - Execute the code under test
- Assert - Verify the results
Anti-Patterns to Avoid
- Testing implementation details - Test behavior, not how it's achieved
- Excessive mocking - If everything is mocked, you're not testing real behavior
- One assertion per test dogma - Multiple related assertions in one test is fine
- Testing private methods directly - Always go through public API
- Duplicating tests for trivial variations - Use parametrized tests for input variations
- Mocking what you own - Prefer real implementations of your own simple classes