home / skills / williamzujkowski / standards / unit-testing
npx playbooks add skill williamzujkowski/standards --skill unit-testingReview the files below or copy the command above to add this skill to your agents.
---
name: unit-testing
description: Unit testing standards following TDD methodology, test pyramid principles, and comprehensive coverage practices. Covers pytest, Jest, mocking, fixtures, and CI integration for reliable test suites.
---
# Unit Testing Standards
> **Quick Navigation:**
> Level 1: [Quick Start](#level-1-quick-start-2000-tokens-5-minutes) (5 min) → Level 2: [Implementation](#level-2-implementation-5000-tokens-30-minutes) (30 min) → Level 3: [Mastery](#level-3-mastery-resources) (Extended)
---
## Level 1: Quick Start (<2,000 tokens, 5 minutes)
### Core Principles
1. **Test-Driven Development (TDD)**: Write tests before implementation (Red-Green-Refactor)
2. **Test Pyramid**: 70% unit tests, 20% integration, 10% E2E
3. **Fast and Isolated**: Tests run in milliseconds, no external dependencies
4. **Comprehensive Coverage**: Aim for 80-90% code coverage minimum
5. **Clear and Maintainable**: Tests serve as living documentation
### Essential Checklist
- [ ] **TDD workflow**: Write failing test → Implement → Refactor
- [ ] **Coverage targets**: 80%+ overall, 95%+ for critical paths
- [ ] **Naming convention**: `test_<function>_<scenario>_<expected_result>`
- [ ] **AAA pattern**: Arrange, Act, Assert structure in every test
- [ ] **Mocking**: Mock external dependencies (database, APIs, filesystem)
- [ ] **Fixtures**: Reusable test data and setup/teardown
- [ ] **Parametrized tests**: Test multiple inputs efficiently
- [ ] **CI integration**: Tests run automatically on every commit
### Quick Example
```python
# pytest unit testing example
import pytest
from datetime import datetime
def calculate_discount(user_age: int, purchase_amount: float) -> float:
"""Calculate discount based on age and purchase amount."""
if user_age < 18:
return 0.0
elif user_age >= 65:
return purchase_amount * 0.15
elif purchase_amount >= 100:
return purchase_amount * 0.10
return 0.0
# Unit tests following TDD
def test_calculate_discount_no_discount_for_minors():
"""Test that users under 18 receive no discount."""
# Arrange
user_age = 16
purchase_amount = 100.0
# Act
discount = calculate_discount(user_age, purchase_amount)
# Assert
assert discount == 0.0
def test_calculate_discount_senior_discount():
"""Test that seniors (65+) receive 15% discount."""
assert calculate_discount(65, 100.0) == 15.0
assert calculate_discount(70, 200.0) == 30.0
def test_calculate_discount_large_purchase():
"""Test that purchases >= $100 receive 10% discount."""
assert calculate_discount(30, 100.0) == 10.0
assert calculate_discount(30, 150.0) == 15.0
@pytest.mark.parametrize("age,amount,expected", [
(16, 100, 0.0), # Minor
(30, 50, 0.0), # No discount
(30, 100, 10.0), # Large purchase
(65, 50, 7.5), # Senior
(70, 200, 30.0), # Senior large purchase
])
def test_calculate_discount_parametrized(age, amount, expected):
"""Test multiple discount scenarios."""
assert calculate_discount(age, amount) == expected
```
### Quick Links to Level 2
- [TDD Workflow](#tdd-workflow)
- [Test Organization](#test-organization)
- [Mocking and Fixtures](#mocking-and-fixtures)
- [Coverage Analysis](#coverage-analysis)
- [Best Practices](#best-practices)
---
## Level 2: Implementation (<5,000 tokens, 30 minutes)
### TDD Workflow
**Red-Green-Refactor Cycle** (see [resources/tdd-workflow.md](resources/tdd-workflow.md))
```python
# Step 1: RED - Write failing test
def test_user_authentication_success():
"""Test successful user authentication."""
auth_service = AuthenticationService()
result = auth_service.authenticate('[email protected]', 'password123')
assert result.success is True
assert result.token is not None
# Step 2: GREEN - Write minimum code to pass
class AuthenticationService:
def authenticate(self, email: str, password: str):
# Minimal implementation
return AuthResult(success=True, token='dummy_token')
# Step 3: REFACTOR - Improve implementation
class AuthenticationService:
def __init__(self, user_repository, token_generator):
self.user_repo = user_repository
self.token_gen = token_generator
def authenticate(self, email: str, password: str):
user = self.user_repo.find_by_email(email)
if not user or not user.verify_password(password):
return AuthResult(success=False, error='Invalid credentials')
token = self.token_gen.generate(user.id)
return AuthResult(success=True, token=token)
```
### Test Organization
**Test Structure** (see [templates/test-template-pytest.py](templates/test-template-pytest.py))
```python
# tests/test_user_service.py
import pytest
from unittest.mock import Mock, MagicMock
from app.services.user_service import UserService
from app.models.user import User
@pytest.fixture
def mock_database():
"""Create mock database connection."""
db = Mock()
db.query = MagicMock(return_value=[])
return db
@pytest.fixture
def user_service(mock_database):
"""Create UserService with mocked dependencies."""
return UserService(database=mock_database)
class TestUserService:
"""Test suite for UserService."""
def test_create_user_success(self, user_service, mock_database):
"""Test successful user creation."""
# Arrange
user_data = {'email': '[email protected]', 'name': 'Test User'}
mock_database.insert = MagicMock(return_value=1)
# Act
user_id = user_service.create_user(user_data)
# Assert
assert user_id == 1
mock_database.insert.assert_called_once()
def test_create_user_duplicate_email(self, user_service, mock_database):
"""Test user creation fails with duplicate email."""
# Arrange
user_data = {'email': '[email protected]', 'name': 'Test'}
mock_database.find_by_email = MagicMock(return_value=User(id=1))
# Act & Assert
with pytest.raises(DuplicateEmailError):
user_service.create_user(user_data)
```
### Mocking and Fixtures
**Advanced Mocking** (see [templates/test-mocking-examples.py](templates/test-mocking-examples.py))
```python
from unittest.mock import Mock, patch, MagicMock
import pytest
# Mock external API calls
@patch('requests.get')
def test_fetch_user_data(mock_get):
"""Test fetching user data from external API."""
# Arrange
mock_response = Mock()
mock_response.json.return_value = {'id': 1, 'name': 'John'}
mock_response.status_code = 200
mock_get.return_value = mock_response
# Act
service = ExternalAPIService()
user_data = service.fetch_user(1)
# Assert
assert user_data['name'] == 'John'
mock_get.assert_called_once_with('https://api.example.com/users/1')
# Mock database operations
@pytest.fixture
def mock_db_session():
"""Create mock database session."""
session = MagicMock()
session.query = MagicMock()
session.add = MagicMock()
session.commit = MagicMock()
return session
def test_save_user(mock_db_session):
"""Test saving user to database."""
repository = UserRepository(session=mock_db_session)
user = User(name='Test', email='[email protected]')
repository.save(user)
mock_db_session.add.assert_called_once_with(user)
mock_db_session.commit.assert_called_once()
```
### Coverage Analysis
**Coverage Configuration** (see [resources/configs/pytest.ini](resources/configs/pytest.ini))
```ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--verbose
--cov=src
--cov-report=term-missing
--cov-report=html
--cov-report=xml
--cov-fail-under=80
[coverage:run]
source = src
omit =
*/tests/*
*/venv/*
*/__pycache__/*
*/site-packages/*
[coverage:report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
if __name__ == .__main__.:
if TYPE_CHECKING:
```
**Running Coverage**
```bash
# Run tests with coverage
pytest --cov=src tests/
# Generate HTML report
pytest --cov=src --cov-report=html tests/
open htmlcov/index.html
# Coverage for specific module
pytest --cov=src.services.user_service tests/test_user_service.py
```
### Best Practices
**Test Naming and Organization**
```python
# ✅ Good: Descriptive test names
def test_calculate_total_with_discount_applies_10_percent_for_loyalty_members():
"""Test that loyalty members receive 10% discount on total."""
pass
# ❌ Bad: Vague test names
def test_calculate():
pass
# ✅ Good: Group related tests
class TestUserAuthentication:
def test_successful_login(self):
pass
def test_failed_login_invalid_password(self):
pass
def test_failed_login_nonexistent_user(self):
pass
# ✅ Good: Test one thing
def test_user_creation_generates_unique_id():
user = create_user('[email protected]')
assert isinstance(user.id, str)
assert len(user.id) == 36 # UUID length
# ❌ Bad: Testing multiple things
def test_user_creation():
user = create_user('[email protected]')
assert user.id is not None
assert user.email == '[email protected]'
assert user.created_at is not None
assert user.is_active is True # Too many assertions
```
**Parameterized Testing**
```python
@pytest.mark.parametrize("input_value,expected_output", [
(0, 0),
(1, 1),
(2, 4),
(3, 9),
(10, 100),
])
def test_square_function(input_value, expected_output):
"""Test square function with multiple inputs."""
assert square(input_value) == expected_output
@pytest.mark.parametrize("email", [
"invalid.email",
"@example.com",
"user@",
"user @example.com",
"",
])
def test_email_validation_rejects_invalid(email):
"""Test email validation rejects invalid formats."""
with pytest.raises(ValidationError):
validate_email(email)
```
### JavaScript/Jest Testing
**Jest Configuration** (see [resources/configs/jest.config.js](resources/configs/jest.config.js))
```javascript
// jest.config.js
module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: [
'src/**/*.{js,jsx}',
'!src/**/*.test.{js,jsx}',
'!src/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
testMatch: ['**/__tests__/**/*.js', '**/?(*.)+(spec|test).js']
};
```
**Jest Testing Example** (see [templates/test-template-jest.js](templates/test-template-jest.js))
```javascript
// src/calculator.test.js
const { add, subtract, divide } = require('./calculator');
describe('Calculator', () => {
describe('add', () => {
test('should add two positive numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('should add negative numbers', () => {
expect(add(-2, -3)).toBe(-5);
});
});
describe('divide', () => {
test('should divide two numbers', () => {
expect(divide(10, 2)).toBe(5);
});
test('should throw error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
// Mock testing
jest.mock('./apiService');
const apiService = require('./apiService');
test('fetches user data successfully', async () => {
const mockUser = { id: 1, name: 'John' };
apiService.getUser.mockResolvedValue(mockUser);
const user = await fetchUser(1);
expect(user).toEqual(mockUser);
expect(apiService.getUser).toHaveBeenCalledWith(1);
});
```
### Go Testing
**Go Test Template** (see [templates/test-template-go.go](templates/test-template-go.go))
```go
// calculator_test.go
package calculator
import (
"testing"
)
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestDivide(t *testing.T) {
result, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5 {
t.Errorf("got %d, want 5", result)
}
}
func TestDivideByZero(t *testing.T) {
_, err := Divide(10, 0)
if err == nil {
t.Error("expected error for division by zero")
}
}
```
---
## Level 3: Mastery Resources
### Advanced Topics
- **[Property-Based Testing](resources/property-based-testing.md)**: Hypothesis, QuickCheck
- **[Mutation Testing](resources/mutation-testing.md)**: Verify test quality
- **[Test Doubles](resources/test-doubles.md)**: Mocks, stubs, spies, fakes
### Templates & Examples
- **[pytest Template](templates/test-template-pytest.py)**: Complete pytest example
- **[Jest Template](templates/test-template-jest.js)**: Jest with mocks
- **[Go Template](templates/test-template-go.go)**: Table-driven tests
### Configuration Files
- **[pytest.ini](resources/configs/pytest.ini)**: pytest configuration
- **[jest.config.js](resources/configs/jest.config.js)**: Jest configuration
- **[.coveragerc](resources/coverage-configs/.coveragerc)**: Coverage config
### Related Skills
- [Integration Testing](../integration-testing/SKILL.md) - API and database testing
- [Secrets Management](../../security/secrets-management/SKILL.md) - Test security
---
## Quick Reference Commands
```bash
# pytest
pytest # Run all tests
pytest tests/test_user.py # Run specific file
pytest -k "test_auth" # Run tests matching pattern
pytest --cov=src --cov-report=html # Coverage report
pytest -v -s # Verbose with stdout
pytest --tb=short # Short traceback
# Jest
npm test # Run all tests
npm test -- --coverage # With coverage
npm test -- --watch # Watch mode
npm test -- user.test.js # Specific file
# Go
go test ./... # All packages
go test -v # Verbose
go test -cover # Coverage
go test -bench=. # Benchmarks
```
---
## Examples
### Basic Usage
```python
// TODO: Add basic example for unit-testing
// This example demonstrates core functionality
```
### Advanced Usage
```python
// TODO: Add advanced example for unit-testing
// This example shows production-ready patterns
```
### Integration Example
```python
// TODO: Add integration example showing how unit-testing
// works with other systems and services
```
See `examples/unit-testing/` for complete working examples.
## Integration Points
This skill integrates with:
### Upstream Dependencies
- **Tools**: pytest, Jest, Go test, unittest
- **Prerequisites**: Basic understanding of testing concepts
### Downstream Consumers
- **Applications**: Production systems requiring unit-testing functionality
- **CI/CD Pipelines**: Automated testing and deployment workflows
- **Monitoring Systems**: Observability and logging platforms
### Related Skills
- [Integration Testing](../../integration-testing/SKILL.md)
- [E2E Testing](../../e2e-testing/SKILL.md)
- [Ci Cd](../../ci-cd/SKILL.md)
### Common Integration Patterns
1. **Development Workflow**: How this skill fits into daily development
2. **Production Deployment**: Integration with production systems
3. **Monitoring & Alerting**: Observability integration points
## Common Pitfalls
### Pitfall 1: Insufficient Testing
**Problem:** Not testing edge cases and error conditions leads to production bugs
**Solution:** Implement comprehensive test coverage including:
- Happy path scenarios
- Error handling and edge cases
- Integration points with external systems
**Prevention:** Enforce minimum code coverage (80%+) in CI/CD pipeline
### Pitfall 2: Hardcoded Configuration
**Problem:** Hardcoding values makes applications inflexible and environment-dependent
**Solution:** Use environment variables and configuration management:
- Separate config from code
- Use environment-specific configuration files
- Never commit secrets to version control
**Prevention:** Use tools like dotenv, config validators, and secret scanners
### Pitfall 3: Ignoring Security Best Practices
**Problem:** Security vulnerabilities from not following established security patterns
**Solution:** Follow security guidelines:
- Input validation and sanitization
- Proper authentication and authorization
- Encrypted data transmission (TLS/SSL)
- Regular security audits and updates
**Prevention:** Use security linters, SAST tools, and regular dependency updates
**Best Practices:**
- Follow established patterns and conventions for unit-testing
- Keep dependencies up to date and scan for vulnerabilities
- Write comprehensive documentation and inline comments
- Use linting and formatting tools consistently
- Implement proper error handling and logging
- Regular code reviews and pair programming
- Monitor production metrics and set up alerts
---
## Validation
- ✅ Token count: Level 1 <2,000, Level 2 <5,000
- ✅ TDD workflow: Complete Red-Green-Refactor cycle
- ✅ Coverage: 80-90% minimum standards
- ✅ Code examples: Python, JavaScript, Go