create-test
#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-patternSe abre en una nueva pestaña
#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.