shipyard-testing
Use when writing tests, structuring test suites, choosing test boundaries, or debugging test quality issues like flakiness, over-mocking, or brittle tests. Also use when deciding between unit/integration/E2E tests, when tests break during refactoring (sign of testing implementation details), or when test setup exceeds 20 lines. Covers AAA structure, DAMP naming, mock boundaries, and the testing pyramid.
What this skill does
<!-- TOKEN BUDGET: 400 lines / ~1200 tokens -->
# Writing Effective Tests
<activation>
## When This Skill Activates
- Writing or modifying test files (`*.test.*`, `*.spec.*`, `*_test.go`, `test_*.py`)
- Setting up test infrastructure or test utilities
- Debugging flaky, brittle, or slow tests
- Deciding between unit, integration, and E2E tests
- Choosing when and how to use mocks, stubs, or fakes
## Natural Language Triggers
- "add tests", "test coverage", "testing strategy", "write tests for", "how should I test"
</activation>
## Overview
Test behaviors through public APIs. Verify state, not interactions.
**Core principle:** If refactoring breaks your tests but not your users, your tests are wrong.
**Relationship to TDD:** The `shipyard:shipyard-tdd` skill covers WHEN to write tests (test-first, red-green-refactor). This skill covers HOW to write tests that are effective, maintainable, and trustworthy.
## The Iron Law
```
TEST BEHAVIORS, NOT IMPLEMENTATIONS
```
A test should break only when the system's observable behavior changes -- never because of refactoring, renaming internals, or restructuring code.
**No exceptions:**
- Don't test private methods
- Don't assert on internal state
- Don't verify method call sequences
- Don't couple tests to data structures users never see
<instructions>
## Test Structure (AAA)
Every test follows Arrange-Act-Assert:
```
Arrange -- Set up preconditions and inputs
Act -- Execute the behavior under test
Assert -- Verify the expected outcome
```
Separate the three sections with blank lines. One Act per test. One logical assertion per test.
<examples>
<example type="good" title="Clear AAA structure, one behavior per test">
```python
def test_expired_subscription_denies_access():
# Arrange
user = create_user(subscription_end=yesterday())
# Act
result = check_access(user, resource="premium-content")
# Assert
assert result.denied is True
assert result.reason == "subscription expired"
```
Clear name, tests one behavior, obvious structure.
</example>
<example type="bad" title="Multiple behaviors, internal state tested">
```python
def test_subscription():
user = create_user(subscription_end=yesterday())
assert check_access(user, "premium-content").denied
user.subscription_end = tomorrow()
assert check_access(user, "premium-content").allowed
assert user.access_log == [("denied", "premium-content"), ("allowed", "premium-content")]
```
Two behaviors in one test, tests internal log, vague name.
</example>
</examples>
### Keep Tests DAMP, Not DRY
Prefer **Descriptive And Meaningful Phrases** over eliminating duplication. Duplicating setup across tests is fine if it makes each test self-contained and readable. Extract shared setup into helpers only when it improves clarity, not to reduce line count.
### No Logic in Tests
Tests are straight-line code. No loops, conditionals, ternaries, or string concatenation. If you need logic, the test is too complex -- split it or simplify the design.
## What to Test
### Test These
- **Behaviors:** What the system does from a user's perspective
- **Edge cases:** Empty inputs, boundaries, overflow, zero, null
- **Error paths:** Invalid input, missing dependencies, timeouts
- **State transitions:** Before and after an operation
### Skip These
- Trivial getters/setters with no logic
- Generated code (protobuf, ORM migrations)
- Configuration files
- Third-party library internals
### Test Via Public APIs
Invoke the system the same way its callers do. If the only way to test something is through a private method, the design needs to change -- extract it into a collaborator with its own public interface.
## Naming Tests
Name tests after the behavior, not the method.
| Good | Bad |
|------|-----|
| `rejects_empty_email_with_validation_error` | `test_validate` |
| `returns_cached_result_when_within_ttl` | `test_cache` |
| `retries_three_times_before_failing` | `test_retry_logic` |
| `grants_access_when_subscription_active` | `test_check_access_method` |
Patterns that work:
- `[action]_[condition]_[expected_result]`
- `should [expected behavior] when [condition]`
- Plain descriptive sentence
A test name containing "and" usually means two tests.
## Choosing Test Level
```dot
digraph test_level {
rankdir=TB;
start [label="What are you testing?", shape=diamond];
pure [label="Pure logic?\nNo I/O, no side effects", shape=diamond];
boundary [label="System boundary?\nDB, API, file, queue", shape=diamond];
journey [label="Critical user journey?\nLogin, checkout, signup", shape=diamond];
unit [label="UNIT TEST\nFast, isolated, many", shape=box, style=filled, fillcolor="#ccffcc"];
integration [label="INTEGRATION TEST\nBoundary focused, fewer", shape=box, style=filled, fillcolor="#ffffcc"];
e2e [label="E2E TEST\nFull stack, minimal", shape=box, style=filled, fillcolor="#ffcccc"];
start -> pure;
pure -> unit [label="yes"];
pure -> boundary [label="no"];
boundary -> integration [label="yes"];
boundary -> journey [label="no"];
journey -> e2e [label="yes"];
journey -> unit [label="no\n(rethink)"];
}
```
**The pyramid:** Many unit tests. Fewer integration tests. Minimal E2E tests.
**Push tests down.** If a behavior can be tested at a lower level, test it there. Lower = faster, more stable, cheaper to maintain. Only go higher when the lower level can't verify the behavior (serialization, wiring, full user flow).
**Duplicate coverage?** If a unit test and integration test verify the same behavior, keep the unit test. Remove the integration test unless it adds confidence about wiring.
## Test Isolation and Doubles
### When to Use Test Doubles
| Use doubles for | Use real objects for |
|----------------|---------------------|
| Network calls (HTTP, gRPC) | In-process collaborators |
| Database queries (in unit tests) | Value objects and data structures |
| Filesystem I/O | Pure functions |
| Non-deterministic sources (time, random) | Deterministic transformations |
| Expensive operations (ML inference, rendering) | Lightweight dependencies |
### Types of Doubles
- **Stub:** Returns canned responses. Use when you need a dependency to return specific data.
- **Fake:** Working implementation with shortcuts (in-memory DB, local file store). Preferred over mocks for complex interactions.
- **Mock:** Records calls and asserts on them. Use sparingly -- only when the interaction IS the behavior (e.g., "sends an email").
### The Mocking Rule
```
Verify STATE over INTERACTIONS.
Mock only when the side effect IS the behavior under test.
```
<examples>
<example type="good" title="Testing resulting state">
```typescript
// Testing that withdrawal updates balance (state)
test('withdrawal reduces account balance', () => {
const account = new Account(100);
account.withdraw(30);
expect(account.balance).toBe(70);
});
```
Tests resulting state -- survives internal refactoring.
</example>
<example type="bad" title="Testing internal interaction">
```typescript
// Testing that withdrawal calls the right methods (interaction)
test('withdrawal calls debit', () => {
const ledger = mock(Ledger);
const account = new Account(100, ledger);
account.withdraw(30);
expect(ledger.debit).toHaveBeenCalledWith(30);
});
```
Tests internal interaction -- breaks if refactored.
</example>
</examples>
## Unchanging Tests
A well-written test changes only when requirements change. It survives:
- **Refactoring** -- internal restructuring, no behavior change
- **Bug fixes** -- new tests added, existing tests unchanged
- **New features** -- unrelated tests unaffected
If your tests break during refactoring, they're testing implementation details. Fix the tests to test behavior instead.
## Clear Failure Messages
A failure message should let you diagnose the problem without reading the test code.
<examples>
<example type="good" title="Descriptive failure message">
```
FAIL: rRelated in Writing & Docs
jax-development
IncludedUse this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
nature-article-writer
IncludedDrafts, rewrites, diagnostically critiques, and style-calibrates primary research manuscripts for Nature and Nature Portfolio journals. Use when the user wants a Nature-style title, summary paragraph or abstract, introduction, results, discussion, methods, figure legends, presubmission enquiry, cover letter, reviewer response, or when a scientific draft sounds generic, jargon-heavy, structurally weak, or AI-ish and needs precise, broad-reader-friendly prose without inventing data, analyses, or references. Best for primary research articles and letters rather than reviews or press releases unless explicitly adapting one.
deckrd
IncludedDocument-driven framework that derives requirements, specifications, implementation plans, and executable tasks from goals through structured AI dialogue. Use when user says "write requirements", "create spec", "plan implementation", "derive tasks", "structure this feature", "break down into tasks", or "document this module". Also use for reverse engineering existing code into docs (/deckrd rev). Do NOT use for direct code writing — use /deckrd-coder after tasks are generated. Do NOT use when the user only wants to run or fix existing code without planning.
clinical-decision-support
IncludedGenerate professional clinical decision support (CDS) documents for pharmaceutical and clinical research settings, including patient cohort analyses (biomarker-stratified with outcomes) and treatment recommendation reports (evidence-based guidelines with decision algorithms). Supports GRADE evidence grading, statistical analysis (hazard ratios, survival curves, waterfall plots), biomarker integration, and regulatory compliance. Outputs publication-ready LaTeX/PDF format optimized for drug development, clinical research, and evidence synthesis.
handling-sf-data
IncludedSalesforce data operations with 130-point scoring. Use this skill to create, update, delete, bulk import/export, generate test data, and clean up org records using sf CLI and anonymous Apex. TRIGGER when: user creates test data, performs bulk import/export, uses sf data CLI commands, needs data factory patterns for Apex tests, or needs to seed/clean records in a Salesforce org. DO NOT TRIGGER when: SOQL query writing only (use querying-soql), Apex test execution (use running-apex-tests), or metadata deployment (use deploying-metadata).
accelint-ac-to-playwright
IncludedConvert and validate acceptance criteria for Playwright test automation. Use when user asks to (1) review/evaluate/check if AC are ready for automation, (2) assess if AC can be converted as-is, (3) validate AC quality for Playwright, (4) turn AC into tests, (5) generate tests from acceptance criteria, (6) convert .md bullets or .feature Gherkin files to Playwright specs, (7) create test automation from requirements. Handles both bullet-style markdown and Gherkin syntax with JSON test plan generation and validation.