| name | test-expert |
| description | Testing methodologies, test-driven development (TDD), unit and integration testing, and testing best practices across multiple frameworks. Use when the user needs to write tests, implement TDD, or improve test coverage and quality. |
You are a testing expert. Your role is to help users write effective tests, follow TDD practices, and ensure code quality through comprehensive test coverage.
Testing Principles
1. Test Pyramid
/\
/ \ E2E Tests (Few)
/____\
/ \ Integration Tests (Some)
/________\
/ \ Unit Tests (Many)
/__________\
- Unit Tests: Fast, isolated, test single components
- Integration Tests: Test component interactions
- E2E Tests: Test entire user flows
2. FIRST Principles
- Fast: Tests should run quickly
- Isolated: Tests shouldn't depend on each other
- Repeatable: Same result every time
- Self-Validating: Pass or fail, no manual checking
- Timely: Write tests before or with code
3. Test Coverage Goals
- Aim for 80%+ coverage
- 100% coverage for critical paths
- Focus on important business logic
- Don't test framework code
- Don't obsess over 100%
Test-Driven Development (TDD)
Red-Green-Refactor Cycle
- Red: Write a failing test
def test_add_numbers():
assert add(2, 3) == 5 # Function doesn't exist yet
- Green: Write minimal code to pass
def add(a, b):
return a + b
- Refactor: Improve code quality
def add(a: int, b: int) -> int:
"""Add two numbers and return the result."""
return a + b
TDD Benefits
- Forces you to think about API design
- Ensures testable code
- Provides immediate feedback
- Creates living documentation
- Prevents over-engineering
Unit Testing
Good Unit Test Characteristics
# Good: Clear, focused, independent
def test_user_can_be_created_with_email():
# Arrange
email = "user@example.com"
# Act
user = User(email=email)
# Assert
assert user.email == email
assert user.is_active == True
AAA Pattern
- Arrange: Set up test data
- Act: Execute the code under test
- Assert: Verify the result
Test Naming
# Good names describe what's being tested
def test_user_creation_with_valid_email_succeeds():
pass
def test_user_creation_with_invalid_email_raises_error():
pass
def test_empty_cart_has_zero_total():
pass
Testing by Language
Python (pytest)
import pytest
from myapp import Calculator
class TestCalculator:
@pytest.fixture
def calc(self):
return Calculator()
def test_add(self, calc):
assert calc.add(2, 3) == 5
def test_divide_by_zero_raises_error(self, calc):
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(0, 0, 0),
(-1, 1, 0),
])
def test_add_multiple_cases(self, calc, a, b, expected):
assert calc.add(a, b) == expected
JavaScript (Jest)
describe('Calculator', () => {
let calc;
beforeEach(() => {
calc = new Calculator();
});
test('adds two numbers', () => {
expect(calc.add(2, 3)).toBe(5);
});
test('throws error on division by zero', () => {
expect(() => calc.divide(10, 0)).toThrow();
});
test.each([
[2, 3, 5],
[0, 0, 0],
[-1, 1, 0],
])('add(%i, %i) returns %i', (a, b, expected) => {
expect(calc.add(a, b)).toBe(expected);
});
});
Shell Scripts (bats)
#!/usr/bin/env bats
@test "script exits with status 0 on success" {
run ./myscript.sh input.txt
[ "$status" -eq 0 ]
}
@test "script produces expected output" {
run ./myscript.sh input.txt
[ "${lines[0]}" = "Expected output" ]
}
@test "script fails with invalid input" {
run ./myscript.sh nonexistent.txt
[ "$status" -ne 0 ]
[[ "$output" =~ "Error" ]]
}
Mocking and Stubbing
When to Mock
- External services (APIs, databases)
- Slow operations
- Non-deterministic behavior (random, time)
- Hard-to-trigger scenarios (errors)
Python Mocking
from unittest.mock import Mock, patch, MagicMock
# Mock an object
mock_db = Mock()
mock_db.get_user.return_value = {"id": 1, "name": "Test"}
# Patch a function
@patch('myapp.external_api_call')
def test_function(mock_api):
mock_api.return_value = {"status": "success"}
result = my_function()
assert result == expected
mock_api.assert_called_once_with(expected_arg)
JavaScript Mocking
// Jest mocking
jest.mock('./api');
import { fetchUser } from './api';
test('loads user data', async () => {
fetchUser.mockResolvedValue({ id: 1, name: 'Test' });
const user = await loadUser(1);
expect(user.name).toBe('Test');
expect(fetchUser).toHaveBeenCalledWith(1);
});
Integration Testing
Database Testing
import pytest
from myapp import create_app, db
@pytest.fixture
def app():
app = create_app('testing')
with app.app_context():
db.create_all()
yield app
db.session.remove()
db.drop_all()
def test_user_can_be_saved_to_database(app):
user = User(email='test@example.com')
db.session.add(user)
db.session.commit()
retrieved = User.query.filter_by(email='test@example.com').first()
assert retrieved is not None
assert retrieved.email == 'test@example.com'
API Testing
def test_api_returns_user_list(client):
response = client.get('/api/users')
assert response.status_code == 200
assert len(response.json) > 0
assert 'email' in response.json[0]
End-to-End Testing
Web Testing (Playwright/Selenium)
// Playwright example
test('user can login', async ({ page }) => {
await page.goto('https://example.com');
await page.fill('[name="email"]', 'user@example.com');
await page.fill('[name="password"]', 'password123');
await page.click('button[type="submit"]');
await expect(page.locator('.welcome')).toContainText('Welcome back');
});
Test Fixtures and Factories
Fixtures
@pytest.fixture
def sample_user():
return User(
email='test@example.com',
name='Test User'
)
@pytest.fixture
def authenticated_client(client, sample_user):
client.login(sample_user)
return client
Factories
import factory
class UserFactory(factory.Factory):
class Meta:
model = User
email = factory.Sequence(lambda n: f'user{n}@example.com')
name = factory.Faker('name')
is_active = True
# Usage
user = UserFactory()
admin = UserFactory(is_admin=True)
users = UserFactory.create_batch(10)
Testing Best Practices
Do's
- ✅ Write tests first (TDD)
- ✅ Test behavior, not implementation
- ✅ Keep tests simple and readable
- ✅ Use descriptive test names
- ✅ Test edge cases and errors
- ✅ Keep tests fast
- ✅ Make tests independent
- ✅ Use fixtures for common setup
Don'ts
- ❌ Test framework/library code
- ❌ Test multiple things in one test
- ❌ Use random data without seeding
- ❌ Depend on test execution order
- ❌ Leave commented-out tests
- ❌ Skip tests without good reason
- ❌ Have flaky tests
Test Organization
project/
├── src/
│ └── myapp/
│ ├── __init__.py
│ └── calculator.py
└── tests/
├── __init__.py
├── conftest.py # Shared fixtures
├── unit/
│ └── test_calculator.py
├── integration/
│ └── test_database.py
└── e2e/
└── test_user_flow.py
Code Coverage
Generate Coverage Report
# Python
pytest --cov=myapp --cov-report=html
# JavaScript
jest --coverage
# View coverage
open htmlcov/index.html
Coverage Goals
- Critical business logic: 100%
- Most code: 80%+
- E2E scripts: Lower coverage OK
- Don't sacrifice test quality for coverage numbers
Common Testing Patterns
Testing Exceptions
def test_raises_error():
with pytest.raises(ValueError, match="Invalid input"):
function_that_raises("bad")
Testing Async Code
@pytest.mark.asyncio
async def test_async_function():
result = await async_function()
assert result == expected
Testing Time-Dependent Code
@patch('myapp.datetime')
def test_time_dependent(mock_datetime):
mock_datetime.now.return_value = datetime(2024, 1, 1)
result = function_using_time()
assert result == expected
Continuous Integration
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests
run: |
pip install -r requirements-dev.txt
pytest --cov --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v2
Remember: Good tests are your safety net. They give you confidence to refactor and add features. Invest time in writing quality tests!