Imported from travisjneuman/.claude (
skills/test-specialist/SKILL.md). Install upstream withnpx skills add travisjneuman/.claude --skill test-specialist. Copyright stays with the author.
Test Specialist
Test-writing patterns for JS/TS, Python, Go, and Rust, plus bug-analysis habits that apply when tests are requested.
When to Use
Use this skill only when the user explicitly asks for tests. Without that request, validate by reviewing the diff and the code it touches.
Use for (when tests are requested):
- Writing unit, integration, or E2E tests
- Improving test coverage
- Security and performance tests
Don't use when:
- Debugging without a test request → use
debug-systematic - Code review → use
generic-code-reviewer - Technical debt → use
tech-debt-analyzer - Feature development → use
generic-feature-developer
Testing Stack by Project
| Project Type | Unit Tests | Component | E2E |
|---|---|---|---|
| React/Next.js | Vitest/Jest | Testing Library | Cypress |
| Node.js | Vitest/Jest | Supertest | Cypress |
| Static | Jest | - | Cypress |
Playwright (@playwright/test, playwright-core, Playwright MCP, Playwright browsers) is prohibited, and so are Puppeteer and Selenium/WebDriver: never add, install, run, or suggest them. For a browser-level suite use Cypress (or the project's existing non-Playwright runner); for components in a real browser, Vitest browser mode with the preview provider, never the playwright or webdriverio providers. Browser test runners, Percy, Chromatic, and npx browser downloads run only when the user asks, through an environment they have approved. To look at a page without a suite, use the active app's built-in browser, then agent-browser.
Test Patterns
Unit Tests (AAA Pattern)
describe("calculateTotal", () => {
test("sums amounts correctly", () => {
// Arrange
const items = [{ amount: 100 }, { amount: 50 }];
// Act
const total = calculateTotal(items);
// Assert
expect(total).toBe(150);
});
test("handles empty list", () => {
expect(calculateTotal([])).toBe(0);
});
});
Component Tests (User Behavior)
// ✅ Test user behavior, not implementation
it('creates item when user clicks Add', async () => {
const user = userEvent.setup();
render(<ItemList />);
await user.click(screen.getByRole('button', { name: /add/i }));
await user.type(screen.getByLabelText(/title/i), 'New item');
await user.click(screen.getByRole('button', { name: /save/i }));
expect(screen.getByText('New item')).toBeInTheDocument();
});
E2E Tests (Cypress)
describe("checkout", () => {
it("user can complete checkout", () => {
cy.visit("/products");
// Add to cart
cy.contains("button", "Add to Cart").click();
cy.contains("a", "Cart").click();
// Checkout
cy.contains("button", "Checkout").click();
cy.get('[name="email"]').type("test@example.com");
cy.contains("button", "Place Order").click();
// Verify
cy.get("h1").should("contain.text", "Order Confirmed");
});
});
Integration Tests
test("POST /items creates item", async () => {
const response = await request(app)
.post("/api/items")
.send({ name: "Test" })
.expect(201);
expect(response.body).toMatchObject({ id: expect.any(Number) });
});
Bug Analysis Process
- Reproduce - Document exact steps, expected vs actual
- Isolate - Binary search, minimal reproduction
- Root Cause - Trace execution, check assumptions, git blame
- Fix - Fix the root cause (write a failing test first only when the user asked for tests)
- Validate - Review the diff and the code paths it touches against the reproduction and edge cases
Debugging Checklist
When debugging an issue:
- Can reproduce consistently
- Minimal reproduction created
- Console/network logs checked
- State at failure point inspected
- Git blame checked for recent changes
- Fix reviewed against the reproduction (failing test only if tests were requested)
Common Bug Patterns
Race Conditions
test("handles concurrent updates", async () => {
const promises = Array.from({ length: 100 }, () => increment());
await Promise.all(promises);
expect(getCount()).toBe(100);
});
Null Safety
test.each([null, undefined, "", 0])("handles invalid input: %p", (input) => {
expect(() => process(input)).toThrow("Invalid");
});
Boundary Values
test("handles edge cases", () => {
expect(paginate([], 1, 10)).toEqual([]); // empty
expect(paginate([item], 1, 10)).toEqual([item]); // single
expect(paginate(items25, 3, 10)).toHaveLength(5); // partial last page
});
Security Tests
test("prevents SQL injection", async () => {
const malicious = "'; DROP TABLE users; --";
await expect(search(malicious)).resolves.not.toThrow();
});
test("sanitizes XSS", () => {
const xss = '<script>alert("xss")</script>';
expect(sanitize(xss)).not.toContain("<script>");
});
test("requires auth", async () => {
await request(app).post("/api/items").expect(401);
});
Performance Tests
test("handles large datasets efficiently", () => {
const largeList = Array.from({ length: 10000 }, (_, i) => ({ value: i }));
const start = performance.now();
process(largeList);
expect(performance.now() - start).toBeLessThan(100);
});
Coverage Targets
| Code Type | Target |
|---|---|
| Critical paths | 90%+ |
| Business logic | 85%+ |
| UI components | 75%+ |
| Utilities | 70%+ |
Test Quality Principles
- One behavior per test
- Descriptive names - test names explain scenario
- Independent tests - no shared state
- Cover edge cases - null, empty, boundaries, errors
- Mock external deps - tests should be fast
- Test behavior - not implementation details
Workflow Decision Tree
| Situation | Action |
|---|---|
| Adding feature (tests requested) | Write test first (TDD) |
| Fixing bug (tests requested) | Write failing test, then fix |
| Improving coverage | Find gaps, prioritize critical paths |
| Code review | Check edge cases, error handling |
Python Testing (pytest)
Fixtures and Parametrize
import pytest
from myapp.services import UserService
@pytest.fixture
def user_service(db_session):
"""Provide a UserService with test database."""
return UserService(session=db_session)
@pytest.fixture
def sample_user(user_service):
"""Create and return a sample user."""
return user_service.create(name="Test User", email="test@example.com")
class TestUserService:
def test_create_user(self, user_service):
user = user_service.create(name="John", email="john@example.com")
assert user.name == "John"
assert user.id is not None
def test_get_user_not_found(self, user_service):
with pytest.raises(UserNotFoundError, match="User 999 not found"):
user_service.get(999)
@pytest.mark.parametrize("email,is_valid", [
("user@example.com", True),
("user@sub.domain.com", True),
("invalid", False),
("@example.com", False),
("", False),
])
def test_validate_email(self, user_service, email: str, is_valid: bool):
assert user_service.validate_email(email) == is_valid
Mocking
from unittest.mock import Mock, patch, AsyncMock
def test_send_notification(user_service):
with patch("myapp.services.email_client") as mock_email:
mock_email.send = Mock(return_value=True)
user_service.notify(user_id=1, message="Hello")
mock_email.send.assert_called_once_with(
to="test@example.com",
body="Hello",
)
# Async mocking
@pytest.mark.asyncio
async def test_fetch_data():
with patch("myapp.client.fetch", new_callable=AsyncMock) as mock_fetch:
mock_fetch.return_value = {"status": "ok"}
result = await process_data()
assert result["status"] == "ok"
conftest.py Patterns
# tests/conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope="session")
def engine():
return create_engine("sqlite:///:memory:")
@pytest.fixture(scope="function")
def db_session(engine):
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.rollback()
session.close()
Base.metadata.drop_all(engine)
Go Testing
Table-Driven Tests
func TestCalculateDiscount(t *testing.T) {
tests := []struct {
name string
amount float64
code string
want float64
wantErr bool
}{
{
name: "valid 10% discount",
amount: 100.0,
code: "SAVE10",
want: 90.0,
},
{
name: "no discount",
amount: 100.0,
code: "",
want: 100.0,
},
{
name: "invalid code",
amount: 100.0,
code: "INVALID",
wantErr: true,
},
{
name: "zero amount",
amount: 0,
code: "SAVE10",
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CalculateDiscount(tt.amount, tt.code)
if (err != nil) != tt.wantErr {
t.Errorf("CalculateDiscount() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("CalculateDiscount() = %v, want %v", got, tt.want)
}
})
}
}
Testify Assertions and Mocks
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
// Mock
type MockUserRepo struct {
mock.Mock
}
func (m *MockUserRepo) FindByID(id string) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func TestGetUser(t *testing.T) {
repo := new(MockUserRepo)
repo.On("FindByID", "123").Return(&User{ID: "123", Name: "John"}, nil)
service := NewUserService(repo)
user, err := service.GetUser("123")
require.NoError(t, err)
assert.Equal(t, "John", user.Name)
repo.AssertExpectations(t)
}
// HTTP handler testing
func TestGetUserHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/users/123", nil)
w := httptest.NewRecorder()
handler := NewHandler(mockService)
handler.GetUser(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var user User
err := json.NewDecoder(w.Body).Decode(&user)
require.NoError(t, err)
assert.Equal(t, "123", user.ID)
}
Rust Testing
Unit and Integration Tests
// src/lib.rs - Unit tests (same file)
pub fn calculate_discount(amount: f64, percentage: f64) -> Result<f64, DiscountError> {
if percentage < 0.0 || percentage > 100.0 {
return Err(DiscountError::InvalidPercentage(percentage));
}
Ok(amount * (1.0 - percentage / 100.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_discount() {
let result = calculate_discount(100.0, 10.0).unwrap();
assert!((result - 90.0).abs() < f64::EPSILON);
}
#[test]
fn test_zero_discount() {
assert_eq!(calculate_discount(100.0, 0.0).unwrap(), 100.0);
}
#[test]
fn test_invalid_percentage() {
assert!(matches!(
calculate_discount(100.0, 150.0),
Err(DiscountError::InvalidPercentage(_))
));
}
#[test]
fn test_negative_percentage() {
assert!(calculate_discount(100.0, -10.0).is_err());
}
}
// tests/integration_test.rs - Integration tests (separate file)
use my_crate::calculate_discount;
#[test]
fn test_full_workflow() {
let original = 200.0;
let discounted = calculate_discount(original, 25.0).unwrap();
assert_eq!(discounted, 150.0);
}
Property-Based Testing (proptest)
use proptest::prelude::*;
proptest! {
#[test]
fn discount_never_exceeds_original(amount in 0.0f64..10000.0, pct in 0.0f64..100.0) {
let result = calculate_discount(amount, pct).unwrap();
prop_assert!(result <= amount);
prop_assert!(result >= 0.0);
}
#[test]
fn roundtrip_serialization(name in "[a-zA-Z]{1,50}", age in 0u32..150) {
let user = User { name: name.clone(), age };
let json = serde_json::to_string(&user).unwrap();
let deserialized: User = serde_json::from_str(&json).unwrap();
prop_assert_eq!(user, deserialized);
}
}
Visual Regression Testing
Cypress Screenshot Comparison
// Uses a local image-snapshot plugin such as @simonsmith/cypress-image-snapshot
describe("homepage visual regression", () => {
it("matches the baseline", () => {
cy.visit("/");
cy.matchImageSnapshot("homepage", { failureThreshold: 0.01, failureThresholdType: "percent" });
});
it("component states", () => {
cy.visit("/components");
// Default state
cy.get(".card").first().matchImageSnapshot("card-default");
// Mobile viewport
cy.viewport(375, 812);
cy.matchImageSnapshot("homepage-mobile");
});
});
// Update baselines: npx --no cypress run --env updateSnapshots=true
Percy (Cloud Visual Testing)
import "@percy/cypress";
it("visual test with Percy", () => {
cy.visit("/dashboard");
cy.percySnapshot("Dashboard");
// Percy compares across browsers and viewport sizes
});
Chromatic (Storybook Visual Testing)
# Run Chromatic on Storybook stories
npx chromatic --project-token=<token>
# CI integration
# Chromatic captures every story as a visual snapshot
# and detects pixel-level changes across PRs
| Tool | Approach | Best For |
|---|---|---|
| Cypress + image snapshot | Local screenshot comparison | E2E visual tests, free |
| Percy | Cloud cross-browser comparison | Multi-browser, team review |
| Chromatic | Storybook story snapshots | Component library visual QA |
See Also
- Code Review Standards - Quality requirements
- Project
CLAUDE.md- Testing rules
