Installs into .claude/skills of the current project.
Are you the author of Loom Testing?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/cosmix-loom-testing)
---
name: loom-testing
description: Test implementation across unit, integration, e2e, security, infrastructure, data pipeline, and ML domains.
allowed-tools:
- Read
- Grep
- Glob
- Edit
- Write
- Bash
triggers:
- test
- testing
- spec
- assert
- expect
- mock
- stub
- spy
- fake
- fixture
- snapshot
- coverage
- TDD
- BDD
- red-green
- regression
- unit test
- integration test
- e2e
- end-to-end
- test suite
- test case
- table-driven
- pytest
- jest
- vitest
- mocha
- junit
- testify
- test framework
---
# Testing
## Overview
Writing tests: unit/integration/e2e plus data-pipeline, ML, and infrastructure domains. This file owns **test-double taxonomy, AAA, and framework patterns**. For pyramid ratios, coverage targets, risk prioritization, and flaky-test diagnosis, see `loom-test-strategy`; for browser/Playwright/Cypress, see `loom-e2e-testing`.
## Workflow
1. **Map the unit** — public interface, dependencies/side effects, invariants, error paths, boundary values.
2. **Pick the altitude** — unit for logic/branches; integration for real collaborators (DB, HTTP) at a boundary; e2e for user journeys. Push detail down the pyramid (`loom-test-strategy`).
3. **Write failing test first** when practical (TDD red→green→refactor): a red test proves the test actually exercises the code; a test that never failed asserts nothing.
4. **Arrange-Act-Assert**, one logical assertion per test, deterministic inputs.
### AAA and naming
- **Arrange** state/doubles → **Act** (one call to the unit) → **Assert** outcome. Blank-line separate the three; more than one Act means split the test.
- Name `test_<unit>_<scenario>_<expected>` — e.g. `test_cart_add_duplicate_increases_quantity`. The name is the spec; a failing name should tell you what broke without reading the body.
- **One logical assert per test.** Multiple physical asserts on *one* behavior are fine (`status`, then `body`); asserting two unrelated behaviors is "assertion roulette" — the first failure masks the rest. Prefer one composite assert (`assert_eq!(got, expected_struct)`) over many field asserts.
## Test Doubles — taxonomy and misuse
Precision matters: "mock" is colloquially any double, but the kind you choose decides whether the test is brittle.
| Double | Does | Verifies | Reach for when |
| ------ | ---- | -------- | -------------- |
| **Dummy** | Fills a parameter, never used | nothing | Satisfy a signature |
| **Stub** | Returns canned values for indirect *inputs* | state (result) | Drive a branch from a dependency's return |
| **Spy** | Stub that records how it was called | state + calls, *after* | Assert an effect happened, loosely |
| **Mock** | Pre-set call *expectations*, self-verifying | interaction, *strict* | The interaction **is** the contract (e.g. "charge called once with amount") |
| **Fake** | Lightweight working impl (in-memory DB, fake clock) | state | Collaborator too slow/awkward for real, but behavior matters |
**When each is wrong:**
- **Mock used where a stub belongs** → the test asserts *how* the code works (which methods it calls), not *what* it produces. Refactoring the internals breaks green tests. Assert the observable outcome; mock only when the call itself is the observable behavior (payment charged, email sent, event published).
- **Over-mocking** → every collaborator faked; the suite is green while real integration is broken. Mock only at architectural seams you don't control: **network, clock, filesystem, randomness, external services**. Do not mock the type under test or pure internal collaborators.
- **Fakes drift** from the real thing (in-memory store enforces no FK constraints the real DB does). Pair fakes with a thin layer of integration/contract tests against the real implementation.
- **Mocking a value object** instead of constructing a real one — always cheaper and truer to build the real value.
## Determinism (non-negotiable)
Flaky tests are worse than no tests — they train the team to ignore red. Eliminate every non-deterministic input:
- **No `sleep`/`waitForTimeout`.** Poll/await a *condition* (element visible, job status == done). A fixed sleep is either flaky (too short) or slow (too long).
- **Freeze the clock.** Inject a clock or use a fake-timer lib; never assert against `Date.now()`/`SystemTime::now()`.
- **Seed RNG** and any faker/UUID source used in assertions; log the seed on failure so you can reproduce.
- **Isolate shared state** — reset DB (transaction rollback or truncate), globals, singletons, env vars, and filesystem between tests. Unique keys (UUID) per test so parallel runs don't collide.
- Order-independent: tests must pass in any order and in isolation (`--shuffle` / random seed in CI).
Diagnosis workflow and the full flakiness cause table live in `loom-test-strategy`.
## Coverage (floor, not target)
Coverage tells you what's **un**tested, not what's tested well. Chasing a number invites assertion-free tests that execute lines without checking anything.
- Track **branch** coverage, not just line — line coverage hides untested `else`/error paths.
- **Mutation testing** (`mutmut`, `cargo-mutants`, StrykerJS) is the real signal: it perturbs code and checks a test fails. Surviving mutants = weak/missing assertions that line coverage rated 100%.
- Cover behavior and error paths, not getters/framework code. Targets by component: see `loom-test-strategy`.
## Framework Patterns
### Python (pytest)
```python
import pytest
from unittest.mock import patch
@pytest.fixture
def db():
d = Database(); d.connect()
yield d
d.disconnect() # teardown runs even on failure
@pytest.mark.parametrize("n,expected", [(0, 1), (1, 1), (5, 120)])
def test_factorial(n, expected): # table-driven: one row per case
assert factorial(n) == expected
@patch("payments.stripe") # patch where it's *used*, not defined
def test_charge_declined_raises(mock_stripe):
mock_stripe.Charge.create.side_effect = CardError("declined")
with pytest.raises(PaymentError, match="declined"):
PaymentProcessor().charge(1000, "tok_x")
```
⚠ `@patch` target is the name in the module under test (`payments.stripe`), not `stripe` itself — patching the wrong path is the #1 mock no-op. Prefer `freezegun`/`time-machine` for the clock and `pytest.raises(match=...)` to assert the message.
### JavaScript/TypeScript (Vitest/Jest)
```javascript
import { describe, it, expect, vi, beforeEach } from "vitest";
describe("UserService", () => {
let db;
beforeEach(() => { db = { query: vi.fn() }; });
it("finds user by email", async () => {
db.query.mockResolvedValue([{ id: 1, email: "a@b.com" }]);
const user = await new UserService(db).findByEmail("a@b.com");
expect(user.id).toBe(1);
expect(db.query).toHaveBeenCalledWith(expect.stringContaining("WHERE email"), ["a@b.com"]);
});
});
```
⚠ Use `vi.useFakeTimers()` for time; `await expect(promise).rejects.toThrow()` for async errors. `toEqual` deep-compares, `toBe` is `Object.is` — mixing them up passes/fails silently on objects. Snapshot tests (`toMatchSnapshot`) rot into rubber-stamps — reserve for stable serialized output, review every snapshot diff, never `--updateSnapshot` blindly.
### Rust (built-in + mockall)
```rust
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::eq;
#[test]
fn fetches_user_from_db() {
let mut db = MockDatabase::new();
db.expect_get_user()
.with(eq(1))
.times(1)
.return_once(|_| Ok(User { id: 1, name: "Alice".into() }));
assert_eq!(UserService::new(db).get_user(1).unwrap().name, "Alice");
}
}
```
⚠ Use `#[should_panic(expected = "...")]` sparingly — prefer returning `Result` and asserting the `Err`. Tests needing serialized access to shared state use `#[serial]` (`serial_test` crate — this repo relies on it). `assert_eq!` prints both sides on failure; hand-rolled `assert!(a == b)` does not.
## Domain Examples
Concrete assertions per domain; strategy/priority layering is in `loom-test-strategy`.
**Data pipeline** — assert on invariants, not exact output:
```python
def test_etl_preserves_rows_and_keys():
out = etl.transform(load_fixture("sales_1000.csv"))
assert len(out) == 1000 # no silent drops
assert out["customer_id"].notna().all() # required field intact
assert out["order_id"].is_unique # no dup fan-out
```
Also test **idempotency** (run twice → same result), incremental (only new rows processed), and partial-failure recovery.
**ML model** — behavioral tests, not just aggregate accuracy:
```python
def test_sentiment_invariant_to_punctuation():
m = load_model("sentiment")
assert abs(m.predict("great product") - m.predict("great product!!!")) < 0.1
```
Cover invariance (irrelevant change → stable output), directional expectation (adding a positive word raises score), and a minimum-functionality set the model must never miss. Guard train/test split for **leakage**.
**Infrastructure (IaC)** — assert on the plan, not a live deploy:
```python
def test_no_public_s3_buckets():
for r in load_plan("main.tfplan.json").resources("aws_s3_bucket"):
assert r.get("acl", "private") != "public-read", f"{r['name']} is public"
```
## Anti-Patterns
- **Testing implementation** — private methods, internal call counts. Test through the public interface; assert outcomes.
- **Interdependent tests** — one test's writes are another's fixture. Each test self-contains its setup.
- **Logic in tests** — loops/conditionals/computed expected values re-implement the code and hide bugs. Hard-code expected values; use parametrization for variants.
- **Excessive mocking** — see taxonomy above; brittle and false-green.
- **Slow unit tests** — real I/O in a "unit" test. Push to integration or fake the boundary.
- **Assertion roulette / no message** — a bare `assert result` failure tells you nothing.
## Test Organization
```text
tests/
├── unit/ # fast, isolated, no I/O
├── integration/ # real DB/HTTP at a boundary
├── e2e/ # user journeys (see loom-e2e-testing)
├── fixtures/ # shared data / factories
└── helpers/ # setup, custom assertions
```
CI altitude: unit on every commit, integration on PR, e2e pre-deploy — details in `loom-test-strategy`.
## Verify before done
- [ ] Each test has one clear reason to fail (one behavior); name states scenario + expected outcome
- [ ] AAA structure; expected values hard-coded, not computed
- [ ] Doubles chosen by role (stub for inputs, mock only where the interaction is the contract); no over-mocking of internals
- [ ] Deterministic: clock frozen, RNG seeded, state reset, no `sleep` — passes shuffled and in isolation
- [ ] Error/edge paths asserted (types **and** messages), not just the happy path
- [ ] New test **fails** when the code under test is broken (verify red before green; spot-check with a mutant)
- [ ] No test-only logic in production code; no leftover `.only`/`fdescribe`/`#[ignore]`