
Testing
- 25 installs
- 4 repo stars
- Updated April 11, 2026
- 89jobrien/steve
testing is a Claude Code skill for test strategy, TDD, test writing, coverage, CI/CD integration, and Playwright web-app testing.
About
testing is a Claude Code skill covering test strategy, automation, TDD, test writing, coverage analysis, and CI/CD integration. It is framework-agnostic with specific guidance for JavaScript, Python, Java, Go, and Rust, and includes Playwright-based web application testing. Developers use it to set up test infrastructure or write unit, integration, and E2E tests. It bundles example scripts and reference files for framework workflows and web testing.
- Covers test strategy, TDD, coverage analysis, CI/CD integration, and web app testing
- Framework-agnostic with per-framework guidance for JS/TS, Python, Java, Go, and Rust
- Includes Playwright web-app testing plus example scripts and reference files
Testing by the numbers
- 25 all-time installs (skills.sh)
- Ranked #1,392 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
testing capabilities & compatibility
- Capabilities
- test strategy · test writing · coverage analysis · e2e testing · ci cd integration
- Works with
- playwright
- Use cases
- testing · ci cd · debugging
What testing says it does
This skill provides comprehensive testing capabilities including test strategy, automation setup, Test-Driven Development (TDD), test writing best practices, coverage analysis, CI/CD integration, and
**Unit Tests**: 70% - Fast, isolated, test individual functions
npx skills add https://github.com/89jobrien/steve --skill testingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 4 |
| Last updated | April 11, 2026 |
| Repository | 89jobrien/steve ↗ |
What it does
Design a test strategy and write unit, integration, and E2E tests across frameworks, including Playwright web-app testing and CI/CD integration.
Who is it for?
Developers setting up test infrastructure or writing unit, integration, and E2E tests across frameworks.
When should I use this skill?
Setting up test infrastructure, writing tests, implementing TDD, analyzing coverage, or testing web apps with Playwright.
What you get
A test strategy and maintainable unit, integration, and E2E tests wired into CI/CD.
- A test strategy
- Unit, integration, and E2E tests
- CI/CD test integration
By the numbers
- Test pyramid 70/20/10 distribution
- 8 core capabilities from strategy to web-app testing
Files
Testing
This skill provides comprehensive testing capabilities including test strategy, automation setup, Test-Driven Development (TDD), test writing best practices, coverage analysis, CI/CD integration, and web application testing with Playwright.
When to Use This Skill
- When setting up test infrastructure for a project
- When creating test strategies and test plans
- When writing unit, integration, or E2E tests
- When implementing TDD/test-first development
- When analyzing test coverage and quality
- When integrating tests into CI/CD pipelines
- When testing web applications with Playwright
- When debugging test failures or improving test reliability
- When writing test fixtures, mock data, or factory functions
- When mocking external dependencies (APIs, databases, file systems)
- When organizing test file structure and test suites
- When testing async code, Promises, or event-driven behavior
- When implementing snapshot tests for UI components
- When configuring test coverage thresholds
What This Skill Does
1. Test Strategy: Designs comprehensive testing strategies (unit, integration, E2E) 2. Test Automation: Sets up test frameworks and automation tools 3. TDD Methodology: Implements Test-Driven Development workflows (Red-Green-Refactor) 4. Test Writing: Writes focused, maintainable tests with proper patterns 5. Coverage Analysis: Analyzes and improves test coverage 6. CI/CD Integration: Integrates tests into continuous integration pipelines 7. Web App Testing: Tests web applications using Playwright 8. Test Quality: Improves test reliability and maintainability
Test Strategy
Test Pyramid
Recommended Distribution:
- Unit Tests: 70% - Fast, isolated, test individual functions
- Integration Tests: 20% - Test component interactions
- E2E Tests: 10% - Test complete user workflows
Test Types:
- Functional tests (happy path, edge cases, error handling)
- Non-functional tests (performance, security, accessibility)
- Regression tests (prevent breaking changes)
- Smoke tests (critical path verification)
Framework Selection
JavaScript/TypeScript:
- Jest, Vitest, Mocha for unit/integration
- Playwright, Cypress for E2E
- React Testing Library for component testing
Python:
- pytest for unit/integration
- Selenium, Playwright for E2E
- unittest for standard library testing
Java:
- JUnit for unit tests
- TestNG for integration
- Selenium for E2E
Go:
- Built-in testing package
- Testify for assertions
Rust:
- Built-in test framework
- Cargo test for running tests
Test-Driven Development (TDD)
TDD is a design technique, not just a testing technique. It produces better-designed, more maintainable code through small, disciplined steps.
Core Principle
Write tests before code. Always. TDD forces you to think about:
- What behavior do I need?
- How will I know it works?
- What's the simplest implementation?
The Three Laws (Never Violate)
1. Write NO production code without a failing test first 2. Write only enough test to demonstrate one failure 3. Write only enough code to pass that test
Red-Green-Refactor Cycle
Phase 1: RED - Write Failing Test
1. Write ONE test that defines desired behavior 2. Run test - verify it FAILS 3. Verify it fails for the RIGHT reason (not syntax error) 4. DO NOT write implementation yet
Phase 2: GREEN - Minimal Implementation
1. Write MINIMAL code to make test pass 2. Resist urge to add extra features 3. Run test - verify it PASSES 4. If test still fails, fix implementation (not test)
Phase 3: REFACTOR - Clean Code
1. Remove code duplication (DRY) 2. Improve naming for clarity 3. Extract complex logic into functions 4. Run ALL tests - must stay green throughout 5. Check test coverage on changed lines
After REFACTOR, start new RED phase for next behavior.
Test Writing Patterns
Arrange-Act-Assert (AAA)
Structure:
1. Arrange: Set up test data and conditions 2. Act: Execute the code being tested 3. Assert: Verify the expected outcome
Example:
describe('UserService', () => {
it('should create user with valid data', async () => {
// Arrange
const userData = { email: 'test@example.com', name: 'Test User' };
// Act
const result = await userService.createUser(userData);
// Assert
expect(result).toHaveProperty('id');
expect(result.email).toBe(userData.email);
});
});Given-When-Then (BDD Style)
Structure:
1. Given: Initial context/preconditions 2. When: Action/event that triggers behavior 3. Then: Expected outcome
Test Organization
File Structure:
project/
├── src/
│ └── components/
│ └── User.jsx
├── tests/
│ ├── unit/
│ │ └── User.test.jsx
│ ├── integration/
│ │ └── UserAPI.test.js
│ └── e2e/
│ └── user-flow.spec.js
├── jest.config.js
└── playwright.config.jsCoverage Analysis
Coverage Goals
Recommended Thresholds:
- Lines: 80%+
- Functions: 80%+
- Branches: 80%+
- Statements: 80%+
Critical Paths:
- Always aim for 100% coverage on critical business logic
- Authentication and authorization
- Payment processing
- Data validation
Coverage Gaps
Common Gaps:
- Error handling paths
- Edge cases
- Boundary conditions
- Integration points
Improvement Strategies:
- Identify untested code paths
- Add tests for error scenarios
- Test edge cases and boundaries
- Increase integration test coverage
CI/CD Integration
Test Pipeline
Stages:
1. Unit Tests: Fast feedback, run on every commit 2. Integration Tests: Run on pull requests 3. E2E Tests: Run before merging to main 4. Performance Tests: Run on main branch
Quality Gates:
- All tests must pass
- Coverage must meet threshold
- No critical security issues
- Performance benchmarks met
Web Application Testing with Playwright
Helper Scripts
This skill includes Python helper scripts in scripts/:
- `with_server.py` - Manages server lifecycle (supports multiple servers). Always run with
--helpfirst to see usage.
# Single server
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py
# Multiple servers (e.g., backend + frontend)
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python your_automation.pyDecision Tree: Choosing Your Approach
User task → Is it static HTML?
├─ Yes → Read HTML file directly to identify selectors
│ ├─ Success → Write Playwright script using selectors
│ └─ Fails/Incomplete → Treat as dynamic (below)
│
└─ No (dynamic webapp) → Is the server already running?
├─ No → Run: python scripts/with_server.py --help
│ Then use the helper + write simplified Playwright script
│
└─ Yes → Reconnaissance-then-action:
1. Navigate and wait for networkidle
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectorsPlaywright Best Practices
- Use bundled scripts as black boxes - Use
--helpto see usage, then invoke directly - Use
sync_playwright()for synchronous scripts - Always close the browser when done
- Use descriptive selectors:
text=,role=, CSS selectors, or IDs - Add appropriate waits:
page.wait_for_selector()orpage.wait_for_timeout() - CRITICAL: Wait for
page.wait_for_load_state('networkidle')before inspection on dynamic apps
Example: Basic Playwright Script
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute
# ... your automation logic
browser.close()Examples
See examples/ directory for:
element_discovery.py- Discovering buttons, links, and inputs on a pagestatic_html_automation.py- Using file:// URLs for local HTMLconsole_logging.py- Capturing console logs during automation
Reference Files
For detailed testing patterns and workflows, load reference files as needed:
- `references/framework_workflows.md` - Framework-specific TDD workflows and examples for Python (pytest), JavaScript (Jest, Vitest), Java (JUnit), Go, Rust
- `references/test_patterns.md` - Common test patterns, test organization, naming conventions, test doubles (mocks, stubs, spies), parametrization, and anti-patterns
- `references/webapp_testing.md` - Web application testing patterns, Playwright best practices, and E2E testing strategies
- `references/TESTING_REPORT.template.md` - Test quality report template with coverage metrics, audit findings, and recommendations
When working with specific frameworks or need detailed patterns, load the appropriate reference file.
Best Practices
Test Quality
1. Isolation: Tests should be independent and runnable in any order 2. Deterministic: Tests should produce consistent results 3. Fast: Unit tests should run quickly (< 100ms each) 4. Clear: Test names should describe what they test 5. Maintainable: Tests should be easy to update when code changes
TDD Best Practices
1. One Behavior Per Test: Each test verifies ONE behavior 2. Descriptive Names: Test names describe the behavior being tested 3. Independent Tests: Tests don't depend on each other 4. Fast Tests: Mock external dependencies to keep tests fast 5. Clear Assertions: Assertions clearly show what's being verified
Common Mistakes to Avoid
- ❌ Writing multiple tests at once (write one test at a time)
- ❌ Skipping refactor phase (always refactor after green)
- ❌ Implementation before test (delete code and start with test)
- ❌ Over-engineering in GREEN (simplest thing that passes)
- ❌ Writing test that passes immediately (must fail first)
Test Maintenance
- Review and update tests when requirements change
- Remove obsolete tests
- Refactor tests to reduce duplication
- Keep test data factories up to date
- Monitor test execution time
Integration with Other Skills
- debugging: Use when tests fail unexpectedly
- code-review: TDD produces code that's easier to review
- dead-code-removal: Tests help identify unused code
- performance: Use for performance testing strategies
Meta-Principle
TDD is a DESIGN technique, not a testing technique.
The cycle never changes: RED → GREEN → REFACTOR → Repeat
Writing tests first forces you to think about:
- What behavior do I need?
- How will I know it works?
- What's the simplest implementation?
This produces better-designed, more maintainable code.from playwright.sync_api import sync_playwright
# Example: Capturing console logs during browser automation
url = "http://localhost:5173" # Replace with your URL
console_logs = []
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1920, "height": 1080})
# Set up console log capture
def handle_console_message(msg):
console_logs.append(f"[{msg.type}] {msg.text}")
print(f"Console: [{msg.type}] {msg.text}")
page.on("console", handle_console_message)
# Navigate to page
page.goto(url)
page.wait_for_load_state("networkidle")
# Interact with the page (triggers console logs)
page.click("text=Dashboard")
page.wait_for_timeout(1000)
browser.close()
# Save console logs to file
with open("/mnt/user-data/outputs/console.log", "w") as f:
f.write("\n".join(console_logs))
print(f"\nCaptured {len(console_logs)} console messages")
print("Logs saved to: /mnt/user-data/outputs/console.log")
import tempfile
from pathlib import Path
from playwright.sync_api import sync_playwright
# Example: Discovering buttons and other elements on a page
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Navigate to page and wait for it to fully load
page.goto("http://localhost:5173")
page.wait_for_load_state("networkidle")
# Discover all buttons on the page
buttons = page.locator("button").all()
print(f"Found {len(buttons)} buttons:")
for i, button in enumerate(buttons):
text = button.inner_text() if button.is_visible() else "[hidden]"
print(f" [{i}] {text}")
# Discover links
links = page.locator("a[href]").all()
print(f"\nFound {len(links)} links:")
for link in links[:5]: # Show first 5
text = link.inner_text().strip()
href = link.get_attribute("href")
print(f" - {text} -> {href}")
# Discover input fields
inputs = page.locator("input, textarea, select").all()
print(f"\nFound {len(inputs)} input fields:")
for input_elem in inputs:
name = input_elem.get_attribute("name") or input_elem.get_attribute("id") or "[unnamed]"
input_type = input_elem.get_attribute("type") or "text"
print(f" - {name} ({input_type})")
# Take screenshot for visual reference
screenshot_path = Path(tempfile.gettempdir()) / "page_discovery.png"
page.screenshot(path=str(screenshot_path), full_page=True)
print(f"\nScreenshot saved to {screenshot_path}")
browser.close()
import os
from playwright.sync_api import sync_playwright
# Example: Automating interaction with static HTML files using file:// URLs
html_file_path = os.path.abspath("path/to/your/file.html")
file_url = f"file://{html_file_path}"
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={"width": 1920, "height": 1080})
# Navigate to local HTML file
page.goto(file_url)
# Take screenshot
page.screenshot(path="/mnt/user-data/outputs/static_page.png", full_page=True)
# Interact with elements
page.click("text=Click Me")
page.fill("#name", "John Doe")
page.fill("#email", "john@example.com")
# Submit form
page.click('button[type="submit"]')
page.wait_for_timeout(500)
# Take final screenshot
page.screenshot(path="/mnt/user-data/outputs/after_submit.png", full_page=True)
browser.close()
print("Static HTML automation completed!")
Framework-Specific TDD Workflows
Detailed TDD workflows and examples for different testing frameworks.
Python - pytest
Setup
Installation:
pip install pytest pytest-cov pytest-mock
# or
uv add pytest pytest-cov pytest-mockConfiguration (`pyproject.toml`):
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"TDD Workflow Example
RED Phase:
# tests/test_calculator.py
def test_add_two_numbers():
result = add(2, 3)
assert result == 5$ pytest tests/test_calculator.py::test_add_two_numbers
FAILED - NameError: name 'add' is not definedGREEN Phase:
# src/calculator.py
def add(a, b):
return 5 # Minimal implementation$ pytest tests/test_calculator.py::test_add_two_numbers
PASSED in 0.01sREFACTOR Phase:
# src/calculator.py
def add(a, b):
return a + b # Proper implementation$ pytest
PASSED - All tests passingCommon Patterns
Fixtures:
@pytest.fixture
def sample_user():
return User(name="Test", email="test@example.com")
def test_user_greeting(sample_user):
assert sample_user.greet() == "Hello, Test"Parametrization:
@pytest.mark.parametrize("a,b,expected", [
(1, 2, 3),
(0, 0, 0),
(-1, 1, 0),
])
def test_add(a, b, expected):
assert add(a, b) == expectedMocking:
def test_api_call(mocker):
mock_get = mocker.patch('requests.get')
mock_get.return_value.json.return_value = {'data': 'test'}
result = fetch_data()
assert result == {'data': 'test'}JavaScript/TypeScript - Jest
Setup
Installation:
npm install --save-dev jest @types/jestConfiguration (`package.json`):
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
},
"jest": {
"testMatch": ["**/__tests__/**/*.js", "**/*.test.js"],
"coverageDirectory": "coverage"
}
}TDD Workflow Example
RED Phase:
// __tests__/calculator.test.js
test('adds two numbers', () => {
const result = add(2, 3);
expect(result).toBe(5);
});$ npm test
FAIL - ReferenceError: add is not definedGREEN Phase:
// src/calculator.js
function add(a, b) {
return 5; // Minimal implementation
}$ npm test
PASS - 1 passedREFACTOR Phase:
// src/calculator.js
function add(a, b) {
return a + b; // Proper implementation
}$ npm test
PASS - All tests passingCommon Patterns
Setup/Teardown:
beforeEach(() => {
user = new User('Test');
});
afterEach(() => {
user.cleanup();
});Mocking:
jest.mock('./api');
test('fetches data', async () => {
api.fetchData.mockResolvedValue({ data: 'test' });
const result = await fetchData();
expect(result).toEqual({ data: 'test' });
});Snapshots:
test('renders component', () => {
const component = render(<MyComponent />);
expect(component).toMatchSnapshot();
});JavaScript/TypeScript - Vitest
Setup
Installation:
npm install --save-dev vitestConfiguration (`vite.config.ts`):
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});TDD Workflow
Similar to Jest, but with Vitest syntax:
import { describe, it, expect } from 'vitest';
describe('calculator', () => {
it('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
});Java - JUnit
Setup
Maven (`pom.xml`):
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
</dependencies>TDD Workflow Example
RED Phase:
// src/test/java/CalculatorTest.java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
@Test
void testAddTwoNumbers() {
int result = Calculator.add(2, 3);
assertEquals(5, result);
}
}GREEN Phase:
// src/main/java/Calculator.java
public class Calculator {
public static int add(int a, int b) {
return 5; // Minimal implementation
}
}REFACTOR Phase:
public class Calculator {
public static int add(int a, int b) {
return a + b; // Proper implementation
}
}Common Patterns
Parameterized Tests:
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testIsPositive(int number) {
assertTrue(number > 0);
}Mocking (Mockito):
@Mock
private UserRepository userRepository;
@Test
void testGetUser() {
when(userRepository.findById(1)).thenReturn(new User(1, "Test"));
User user = userService.getUser(1);
assertEquals("Test", user.getName());
}Go - testing
Setup
Go testing is built-in, no setup needed.
TDD Workflow Example
RED Phase:
// calculator_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Expected 5, got %d", result)
}
}$ go test
FAIL - undefined: AddGREEN Phase:
// calculator.go
package main
func Add(a, b int) int {
return 5 // Minimal implementation
}$ go test
PASSREFACTOR Phase:
func Add(a, b int) int {
return a + b // Proper implementation
}$ go test
PASSCommon Patterns
Table-Driven Tests:
func TestAdd(t *testing.T) {
tests := []struct {
a, b, expected int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tt := range tests {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; expected %d", tt.a, tt.b, result, tt.expected)
}
}
}Subtests:
func TestAdd(t *testing.T) {
t.Run("positive numbers", func(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Expected 5, got %d", result)
}
})
t.Run("negative numbers", func(t *testing.T) {
result := Add(-1, -2)
if result != -3 {
t.Errorf("Expected -3, got %d", result)
}
})
}Rust - cargo test
Setup
Rust testing is built-in, no setup needed.
TDD Workflow Example
RED Phase:
// src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}$ cargo test
FAIL - cannot find function `add` in this scopeGREEN Phase:
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
5 // Minimal implementation
}$ cargo test
PASSREFACTOR Phase:
pub fn add(a: i32, b: i32) -> i32 {
a + b // Proper implementation
}$ cargo test
PASSCommon Patterns
Integration Tests:
// tests/integration_test.rs
use my_crate::add;
#[test]
fn test_add_integration() {
assert_eq!(add(2, 3), 5);
}Property-Based Testing (proptest):
use proptest::prelude::*;
proptest! {
#[test]
fn test_add_commutative(a in 0i32..1000, b in 0i32..1000) {
assert_eq!(add(a, b), add(b, a));
}
}Running Tests
Python (pytest)
pytest # Run all tests
pytest path/to/test.py # Run specific file
pytest -k "test_name" # Run by name pattern
pytest -v # Verbose output
pytest --cov # With coverageJavaScript (Jest)
npm test # Run all tests
npm test -- file.test.js # Run specific file
npm test -- --watch # Watch mode
npm test -- --coverage # With coverageJava (Maven)
mvn test # Run all tests
mvn test -Dtest=TestClass # Run specific classGo
go test # Run all tests
go test ./... # Run all tests recursively
go test -v # Verbose output
go test -cover # With coverageRust
cargo test # Run all tests
cargo test test_name # Run specific test
cargo test -- --nocapture # Show outputTDD Patterns Reference
Common patterns, conventions, and anti-patterns for Test-Driven Development across different languages and frameworks.
Test Organization Patterns
File Structure
Python (pytest):
project/
├── src/
│ └── calculator.py
├── tests/
│ ├── conftest.py # Shared fixtures
│ └── test_calculator.py
└── pyproject.tomlJavaScript/TypeScript (Jest/Vitest):
project/
├── src/
│ └── calculator.js
├── __tests__/
│ └── calculator.test.js
└── package.jsonJava (JUnit):
project/
├── src/
│ └── main/java/Calculator.java
└── src/
└── test/java/CalculatorTest.javaGo:
project/
├── calculator.go
└── calculator_test.goRust:
project/
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rsNaming Conventions
Test Files
Python:
test_*.pyor*_test.py- Example:
test_calculator.py,calculator_test.py
JavaScript/TypeScript:
*.test.js,*.test.ts*.spec.js,*.spec.ts- Example:
calculator.test.js
Java:
*Test.java- Example:
CalculatorTest.java
Go:
*_test.go- Example:
calculator_test.go
Rust:
*_test.rs(unit tests in same file)tests/*.rs(integration tests)- Example:
calculator_test.rs
Test Functions/Methods
General Pattern:
- Start with
test_orTestprefix - Describe behavior being tested
- Use descriptive names:
test_user_can_login_with_valid_credentials
Examples:
test_calculate_total_with_multiple_itemstest_login_fails_with_invalid_passwordtest_user_cannot_access_admin_without_permission
Test Structure Patterns
AAA Pattern (Arrange-Act-Assert)
Structure:
def test_behavior():
# Arrange: Set up test data
user = User(name="Alice")
# Act: Execute the code
result = user.greet()
# Assert: Verify outcome
assert result == "Hello, Alice"Given-When-Then (BDD)
Structure:
def test_behavior():
# Given: Initial context
user = create_user(role="admin")
# When: Action occurs
has_access = user.can_access("/admin")
# Then: Expected outcome
assert has_access is TrueSetup/Teardown Pattern
Python (pytest fixtures):
@pytest.fixture
def user():
user = User(name="Test")
yield user
user.cleanup() # TeardownJavaScript (Jest):
beforeEach(() => {
user = new User('Test');
});
afterEach(() => {
user.cleanup();
});Test Doubles Patterns
Mock Pattern
When to Use:
- External API calls
- Database operations
- File system operations
- Expensive operations
Python (pytest-mock):
def test_api_call(mocker):
mock_request = mocker.patch('requests.get')
mock_request.return_value.json.return_value = {'data': 'test'}
result = fetch_data()
assert result == {'data': 'test'}JavaScript (Jest):
jest.mock('./api');
test('api call', () => {
api.fetchData.mockResolvedValue({ data: 'test' });
const result = await fetchData();
expect(result).toEqual({ data: 'test' });
});Stub Pattern
When to Use:
- Replace dependencies with simple return values
- Control test data
Example:
def test_with_stub():
stub_db = StubDatabase()
stub_db.set_return_value('get_user', User(id=1))
service = UserService(stub_db)
user = service.get_user(1)
assert user.id == 1Spy Pattern
When to Use:
- Verify method calls without replacing behavior
- Count invocations
Example:
def test_logs_message(mocker):
spy_log = mocker.spy(logging, 'info')
process_data()
spy_log.assert_called_once_with('Processing started')Parametrization Patterns
Data-Driven Tests
Python (pytest):
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("world", "WORLD"),
("", ""),
])
def test_uppercase(input, expected):
assert input.upper() == expectedJavaScript (Jest):
test.each([
['hello', 'HELLO'],
['world', 'WORLD'],
['', ''],
])('uppercase %s', (input, expected) => {
expect(input.toUpperCase()).toBe(expected);
});Test Categories
Unit Tests
- Test individual functions/methods
- Fast execution (< 1ms per test)
- No external dependencies
- Mock all external calls
Integration Tests
- Test component interactions
- May use real dependencies (database, APIs)
- Slower than unit tests
- Test workflows, not individual functions
End-to-End Tests
- Test complete user workflows
- Use real systems
- Slowest tests
- Fewest in number (test pyramid)
Anti-Patterns
❌ Testing Implementation Details
Bad:
def test_internal_state():
assert user._internal_counter == 5 # Testing private attributeGood:
def test_behavior():
assert user.is_ready() # Testing public behavior❌ Over-Mocking
Bad:
def test_calculation(mocker):
mocker.patch('math.add')
mocker.patch('math.multiply')
# Mocking everything, testing nothingGood:
def test_calculation():
result = calculate(2, 3)
assert result == 6 # Test actual behavior❌ Test Interdependence
Bad:
# Test 1
def test_create_user():
user = User.create() # Creates global state
assert user.id == 1
# Test 2
def test_get_user():
user = User.get(1) # Depends on test 1
assert user is not NoneGood:
# Each test is independent
def test_create_user():
user = User.create()
assert user.id is not None
def test_get_user():
user = User.create() # Creates its own data
retrieved = User.get(user.id)
assert retrieved.id == user.id❌ Testing Multiple Things
Bad:
def test_user():
user = User.create()
assert user.name == "Test"
assert user.email == "test@example.com"
assert user.is_active is True
assert user.can_login() is True
# Too many assertions, unclear what's being testedGood:
def test_user_creation():
user = User.create(name="Test")
assert user.name == "Test"
def test_user_email():
user = User.create(email="test@example.com")
assert user.email == "test@example.com"
def test_user_can_login():
user = User.create()
assert user.can_login() is True❌ Slow Tests
Bad:
def test_api_call():
result = real_api_call() # Slow network call
assert result is not NoneGood:
def test_api_call(mocker):
mock_api = mocker.patch('api.call')
mock_api.return_value = {'data': 'test'}
result = fetch_data()
assert result == {'data': 'test'}Test Quality Indicators
Good Tests
- ✅ Fast (< 1ms for unit tests)
- ✅ Independent (no shared state)
- ✅ Repeatable (same result every time)
- ✅ Self-validating (clear pass/fail)
- ✅ Timely (written before code)
- ✅ Clear names (describe behavior)
- ✅ One assertion per behavior
Bad Tests
- ❌ Slow (> 100ms for unit tests)
- ❌ Dependent on other tests
- ❌ Flaky (sometimes pass, sometimes fail)
- ❌ Unclear what's being tested
- ❌ Written after code
- ❌ Vague names (
test1,test_function) - ❌ Multiple unrelated assertions
TDD Workflow Patterns
Micro-Cycle Pattern
1. RED: Write smallest failing test 2. GREEN: Write minimal code to pass 3. REFACTOR: Clean up code 4. Repeat: Next smallest test
Feature Cycle Pattern
1. RED: Write test for feature 2. GREEN: Implement feature 3. REFACTOR: Improve implementation 4. RED: Write test for edge case 5. GREEN: Handle edge case 6. REFACTOR: Clean up
Bug Fix Pattern
1. RED: Write test that reproduces bug 2. GREEN: Fix bug (test now passes) 3. REFACTOR: Improve fix if needed 4. Verify: Ensure no regressions
Pytest Test Quality Report
Generated: {{TIMESTAMP}} Project: {{PROJECT_PATH}} Branch: {{GIT_BRANCH}} Python: {{PYTHON_VERSION}} | Pytest: {{PYTEST_VERSION}}
---
Executive Summary
| Metric | Value |
|---|---|
| Status | {{STATUS}} |
| Total Tests | {{TOTAL_TESTS}} |
| Pass Rate | {{PASS_RATE}}% |
| Coverage | {{COVERAGE_PERCENT}}% |
| Issues Found | {{TOTAL_ISSUES}} |
---
Test Execution Summary
Command Executed:
{{PYTEST_COMMAND}}| Result | Count |
|---|---|
| Passed | {{PASSED_COUNT}} |
| Failed | {{FAILED_COUNT}} |
| Skipped | {{SKIPPED_COUNT}} |
| Errors | {{ERROR_COUNT}} |
| Total | {{TOTAL_TESTS}} |
Duration: {{DURATION}}
Test Output
<details> <summary>Click to expand full pytest output</summary>
{{PYTEST_OUTPUT}}</details>
---
Coverage Summary
Overall Coverage: {{COVERAGE_PERCENT}}% Threshold: {{COVERAGE_THRESHOLD}}% Status: {{COVERAGE_STATUS}}
Coverage by Module
| Module | Statements | Missing | Excluded | Coverage |
|---|
{{COVERAGE_TABLE}}
Files Below Threshold
{{FILES_BELOW_THRESHOLD}}
Uncovered Critical Code
{{UNCOVERED_CRITICAL_CODE}}
---
Audit Findings
Total Issues: {{TOTAL_ISSUES}} (Critical: {{CRITICAL_COUNT}}, High: {{HIGH_COUNT}}, Medium: {{MEDIUM_COUNT}}, Low: {{LOW_COUNT}})
CRITICAL (Must Fix)
{{CRITICAL_ISSUES}}
HIGH (Should Fix)
{{HIGH_ISSUES}}
MEDIUM (Improvements)
{{MEDIUM_ISSUES}}
LOW (Optional)
{{LOW_ISSUES}}
---
Test File Inventory
| File | Tests | Passed | Failed | Skipped | Duration |
|---|
{{TEST_FILE_TABLE}}
---
Recommendations
Immediate Actions (Critical/High)
{{IMMEDIATE_ACTIONS}}
Short-term Improvements (Medium)
{{SHORT_TERM_ACTIONS}}
Future Enhancements (Low)
{{FUTURE_ACTIONS}}
---
Coverage Details
Missing Coverage by File
{{MISSING_COVERAGE_DETAILS}}
Suggested Tests to Add
{{SUGGESTED_TESTS}}
---
Test Quality Metrics
| Metric | Value | Target | Status |
|---|---|---|---|
| Coverage | {{COVERAGE_PERCENT}}% | {{COVERAGE_THRESHOLD}}% | {{COVERAGE_STATUS}} |
| Pass Rate | {{PASS_RATE}}% | 100% | {{PASS_RATE_STATUS}} |
| Avg Test Duration | {{AVG_TEST_DURATION}} | <1s | {{DURATION_STATUS}} |
| Tests with Assertions | {{TESTS_WITH_ASSERTIONS}} | 100% | {{ASSERTION_STATUS}} |
| Fixture Usage | {{FIXTURE_USAGE}} | - | - |
| Parametrized Tests | {{PARAMETRIZED_COUNT}} | - | - |
---
Appendix
A. Failed Test Details
{{FAILED_TEST_DETAILS}}
B. Slow Tests (>1s)
{{SLOW_TESTS}}
C. Coverage Report (Raw)
<details> <summary>Click to expand coverage JSON</summary>
{{COVERAGE_JSON}}</details>
---
Report Location: {{REPORT_PATH}} Next Report: Run /tdd-pytest:report to regenerate
Web Application Testing
Comprehensive guide for testing web applications using Playwright and browser automation.
Playwright Basics
Installation
pip install playwright
playwright install chromiumBasic Script Structure
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS
# ... your test logic
browser.close()Server Management
Using with_server.py Helper
The scripts/with_server.py helper manages server lifecycle automatically:
Single server:
python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_test.pyMultiple servers:
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python your_test.pyAlways run with `--help` first to see current usage:
python scripts/with_server.py --helpDecision Tree
User task → Is it static HTML?
├─ Yes → Read HTML file directly to identify selectors
│ ├─ Success → Write Playwright script using selectors
│ └─ Fails/Incomplete → Treat as dynamic (below)
│
└─ No (dynamic webapp) → Is the server already running?
├─ No → Run: python scripts/with_server.py --help
│ Then use the helper + write simplified Playwright script
│
└─ Yes → Reconnaissance-then-action:
1. Navigate and wait for networkidle
2. Take screenshot or inspect DOM
3. Identify selectors from rendered state
4. Execute actions with discovered selectorsReconnaissance-Then-Action Pattern
Step 1: Inspect Rendered DOM
# Navigate and wait for full load
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
# Take screenshot for visual reference
page.screenshot(path='/tmp/inspect.png', full_page=True)
# Get page content
content = page.content()
# Discover elements
buttons = page.locator('button').all()
links = page.locator('a[href]').all()
inputs = page.locator('input, textarea, select').all()Step 2: Identify Selectors
Use multiple selector strategies:
text=- Match by visible textrole=- Match by ARIA role- CSS selectors - Standard CSS selectors
- IDs - Element IDs
Best Practice: Prefer text= and role= over CSS selectors for better maintainability.
Step 3: Execute Actions
# Click button by text
page.click('text=Submit')
# Fill form field
page.fill('#email', 'test@example.com')
# Select dropdown
page.select_option('#country', 'US')
# Wait for element
page.wait_for_selector('.success-message')Common Patterns
Element Discovery
# Discover all buttons
buttons = page.locator('button').all()
for i, button in enumerate(buttons):
text = button.inner_text() if button.is_visible() else "[hidden]"
print(f" [{i}] {text}")
# Discover links
links = page.locator('a[href]').all()
for link in links:
text = link.inner_text().strip()
href = link.get_attribute('href')
print(f" - {text} -> {href}")
# Discover input fields
inputs = page.locator('input, textarea, select').all()
for input_elem in inputs:
name = input_elem.get_attribute('name') or input_elem.get_attribute('id')
input_type = input_elem.get_attribute('type') or 'text'
print(f" - {name} ({input_type})")Static HTML Testing
import os
html_file_path = os.path.abspath('path/to/your/file.html')
file_url = f'file://{html_file_path}'
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
# Navigate to local HTML file
page.goto(file_url)
# Interact with elements
page.click('text=Click Me')
page.fill('#name', 'John Doe')
browser.close()Console Log Capture
console_logs = []
def handle_console_message(msg):
console_logs.append(f"[{msg.type}] {msg.text}")
print(f"Console: [{msg.type}] {msg.text}")
page.on("console", handle_console_message)
# Navigate and interact
page.goto(url)
page.wait_for_load_state('networkidle')
page.click('text=Dashboard')
# Save logs
with open('console.log', 'w') as f:
f.write('\n'.join(console_logs))Best Practices
Waiting Strategies
Always wait for networkidle on dynamic apps:
page.goto(url)
page.wait_for_load_state('networkidle') # CRITICALWait for specific elements:
page.wait_for_selector('.loading-spinner', state='hidden')
page.wait_for_selector('.success-message', state='visible')Wait for navigation:
with page.expect_navigation():
page.click('text=Submit')Selector Best Practices
Prefer semantic selectors:
# Good: Text-based selector
page.click('text=Submit Form')
# Good: Role-based selector
page.click('role=button[name="Submit"]')
# Acceptable: CSS selector
page.click('#submit-button')
# Avoid: Fragile CSS selectors
page.click('div.container > div.row > div.col > button.btn-primary')Error Handling
try:
page.goto(url)
page.wait_for_load_state('networkidle', timeout=10000)
except Exception as e:
print(f"Error loading page: {e}")
page.screenshot(path='error.png')
raiseScreenshots
# Full page screenshot
page.screenshot(path='full_page.png', full_page=True)
# Element screenshot
element = page.locator('.widget')
element.screenshot(path='widget.png')
# Screenshot on failure
try:
page.click('text=Submit')
except Exception:
page.screenshot(path='failure.png')
raiseCommon Pitfalls
❌ Don't Inspect Before networkidle
Bad:
page.goto(url)
content = page.content() # Too early! JS hasn't executedGood:
page.goto(url)
page.wait_for_load_state('networkidle') # Wait first
content = page.content() # Now safe❌ Don't Use Hardcoded Timeouts
Bad:
page.click('text=Submit')
time.sleep(5) # Arbitrary waitGood:
page.click('text=Submit')
page.wait_for_selector('.success-message') # Wait for actual condition❌ Don't Forget to Close Browser
Bad:
browser = p.chromium.launch()
page = browser.new_page()
# ... test code
# Browser never closed!Good:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# ... test code
browser.close() # Automatically closed by context managerExamples
See examples/ directory for complete examples:
element_discovery.py- Discovering buttons, links, and inputsstatic_html_automation.py- Testing static HTML filesconsole_logging.py- Capturing console logs during automation
Integration with Test Frameworks
With pytest
import pytest
from playwright.sync_api import sync_playwright
@pytest.fixture(scope="module")
def browser():
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
yield browser
browser.close()
def test_login(browser):
page = browser.new_page()
page.goto('http://localhost:5173')
page.wait_for_load_state('networkidle')
page.fill('#email', 'test@example.com')
page.fill('#password', 'password')
page.click('text=Login')
page.wait_for_selector('.dashboard')
assert page.locator('.dashboard').is_visible()
page.close()With unittest
import unittest
from playwright.sync_api import sync_playwright
class WebAppTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.playwright = sync_playwright().start()
cls.browser = cls.playwright.chromium.launch(headless=True)
@classmethod
def tearDownClass(cls):
cls.browser.close()
cls.playwright.stop()
def setUp(self):
self.page = self.browser.new_page()
def tearDown(self):
self.page.close()
def test_homepage_loads(self):
self.page.goto('http://localhost:5173')
self.page.wait_for_load_state('networkidle')
self.assertIn('Welcome', self.page.content())#!/usr/bin/env python3
"""Start one or more servers, wait for them to be ready, run a command, then clean up.
Usage:
# Single server
python scripts/with_server.py --server "npm run dev" --port 5173 -- python automation.py
python scripts/with_server.py --server "npm start" --port 3000 -- python test.py
# Multiple servers
python scripts/with_server.py \
--server "cd backend && python server.py" --port 3000 \
--server "cd frontend && npm run dev" --port 5173 \
-- python test.py
"""
import argparse
import socket
import subprocess
import sys
import time
def is_server_ready(port, timeout=30):
"""Wait for server to be ready by polling the port."""
start_time = time.time()
while time.time() - start_time < timeout:
try:
with socket.create_connection(("localhost", port), timeout=1):
return True
except (OSError, ConnectionRefusedError):
time.sleep(0.5)
return False
def main():
parser = argparse.ArgumentParser(description="Run command with one or more servers")
parser.add_argument(
"--server",
action="append",
dest="servers",
required=True,
help="Server command (can be repeated)",
)
parser.add_argument(
"--port",
action="append",
dest="ports",
type=int,
required=True,
help="Port for each server (must match --server count)",
)
parser.add_argument(
"--timeout",
type=int,
default=30,
help="Timeout in seconds per server (default: 30)",
)
parser.add_argument(
"command", nargs=argparse.REMAINDER, help="Command to run after server(s) ready"
)
args = parser.parse_args()
# Remove the '--' separator if present
if args.command and args.command[0] == "--":
args.command = args.command[1:]
if not args.command:
print("Error: No command specified to run")
sys.exit(1)
# Parse server configurations
if len(args.servers) != len(args.ports):
print("Error: Number of --server and --port arguments must match")
sys.exit(1)
servers = []
for cmd, port in zip(args.servers, args.ports, strict=False):
servers.append({"cmd": cmd, "port": port})
server_processes = []
try:
# Start all servers
for i, server in enumerate(servers):
print(f"Starting server {i + 1}/{len(servers)}: {server['cmd']}")
# Use shell=True to support commands with cd and &&
# Security: cmd comes from user-controlled config, validated at parse time
process = subprocess.Popen(
server["cmd"],
shell=True, # nosec B602
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
server_processes.append(process)
# Wait for this server to be ready
print(f"Waiting for server on port {server['port']}...")
if not is_server_ready(server["port"], timeout=args.timeout):
raise RuntimeError(
f"Server failed to start on port {server['port']} within {args.timeout}s"
)
print(f"Server ready on port {server['port']}")
print(f"\nAll {len(servers)} server(s) ready")
# Run the command
print(f"Running: {' '.join(args.command)}\n")
result = subprocess.run(args.command, check=False)
sys.exit(result.returncode)
finally:
# Clean up all servers
print(f"\nStopping {len(server_processes)} server(s)...")
for i, process in enumerate(server_processes):
try:
process.terminate()
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
print(f"Server {i + 1} stopped")
print("All servers stopped")
if __name__ == "__main__":
main()
Related skills
FAQ
Is it tied to one framework?
No. It is framework-agnostic with framework-specific guidance via reference files for JS/TS, Python, Java, Go, and Rust.
What test distribution does it recommend?
A test pyramid of roughly 70% unit, 20% integration, and 10% E2E tests.