create-test — Claude skill
Author tests that verify behavior through the public API. Pick the test type that fits the project's maturity, structure every test as Arrange Act Assert separated by blank lines, tend to one assertion per test, prefer a setup function at the bottom of the file over beforeEach, and use object mothers for repeated fixtures. Use when writing, improving, or reviewing unit, integration, or characterization tests, when the user says "add tests", "write a test", "test this", or invokes /create-test. Not for bootstrapping an E2E harness, driving a full feature slice test-first, or running the suite (use validate).
#create-test
Testing is easy; the challenging part is having testable code. A test earns trust by verifying behavior through the public API, the way a consumer of the code would, never by inspecting internals.
#When to use
Use when authoring or improving tests: choosing what kind of test to write, writing the test itself, or cleaning up an existing suite that tests internals instead of behavior.
Not for: bootstrapping E2E harnesses or user journeys, driving a feature slice test-first, or running the full validation suite (use validate).
#Choose the test type first
Before writing a line of test code, decide what is most beneficial to the project right now.
- Unit: one piece of logic in isolation, fast and focused.
- Integration: several pieces working together, like your code against a real database.
- E2E: the whole system from the outside, like a user would.
- Characterization: captures what legacy code currently does, so you can refactor without changing behavior.
- Performance: speed and load against a defined threshold.
- Property-based: a rule that must always hold, checked against hundreds of generated inputs.
- Mutation: small bugs introduced into the code to check the tests catch them.
- Regression: locks in a fixed bug so it can never come back.
If the project has no tests, start with E2E on the happy paths: it tests the resulting artifact instead of the internals, so it delivers value even when the code is not yet testable. From there progress in this order: characterization, unit, integration, then mutation, property-based, regression, and performance as the suite matures.
If the project already has tests, match its current stage and add whatever is most valuable now. There is no universal answer; test what makes sense.
#Test behavior, not internals
- Exercise only the public API. No
as anyto reach private state, no spying on private methods. - Coverage measures execution, not verification. A test that calls code without asserting covers 100% of it and proves nothing. Never chase a coverage number.
- Name the test after the behavior: "applies a 10% discount to the total when a valid coupon is used", never "works".
// A great test: verifies behavior through the public API
it('applies a 10% discount to the total when a valid coupon is used', () => {
const cart = new Cart();
cart.add({ name: 'Keyboard', price: 100 });
cart.add({ name: 'Mouse', price: 50 });
cart.applyCoupon('SAVE10');
expect(cart.total()).toBe(135);
});
// A poor test: coupled to internals, verifies nothing the user cares about
it('works', () => {
const cart = new Cart();
const spy = vi.spyOn(cart as any, 'recalculate');
cart.add({ name: 'Keyboard', price: 100 });
expect(spy).toHaveBeenCalledTimes(1);
expect((cart as any).items.length).toBe(1);
});
#Structure: Arrange, Act, Assert
- Separate the three groups with a single blank line, no comment labels: arrange (mocks, instances, data), act (run the subject under test), assert (verify).
- Tend to one assertion per test.
- When the arrange block repeats across tests, extract a
setup()function at the bottom of the file. Prefer it overbeforeEach: less code, no shared mutable state, and configurable through a defaulted options object.
function setup({ items = [{ name: 'Keyboard', price: 100 }, { name: 'Mouse', price: 50 }] } = {}) {
const cart = new Cart();
items.forEach(item => cart.add(item));
return cart;
}
- When the same mocks or fixtures repeat across files, use the object mother pattern: https://cesalberca.com/blog/mother-design-patternOpen in a new tab
#Validation
- Run the project's test command; the new tests pass.
- Break check: flip or comment out the implementation the test targets; the test must fail. A test that cannot fail verifies nothing.
- Each test reads as AAA at a glance and asserts observable behavior, not internal calls.
#Traps
- Coverage as the goal. Near 100% coverage and still no trust in the code. Assert outcomes, not execution.
- Spying on internals. Refactors break tests even though behavior never changed. Test through the public API.
beforeEachwith shared mutable state. More code and hidden coupling between tests. Use asetup()function.- Vague names. A failing "works" tells you nothing. Name the behavior and its condition.
- Many unrelated assertions in one test. The first failure hides the rest. Split the test.