Skip to content

Instantly share code, notes, and snippets.

@antarikshc
Created April 23, 2026 06:52
Show Gist options
  • Select an option

  • Save antarikshc/7a65bf495610dc24e872f53af05fbfa4 to your computer and use it in GitHub Desktop.

Select an option

Save antarikshc/7a65bf495610dc24e872f53af05fbfa4 to your computer and use it in GitHub Desktop.
unit-testing skill
name unit-testing
description Guide for generating, reviewing, and planning unit tests. Use when writing tests from scratch, improving existing tests, or identifying what to test. Framework-agnostic best practices for writing effective, maintainable unit tests.

Unit Testing

Overview

Write tests that verify behavior, not implementation. A good test catches real bugs, reads like documentation, and survives refactoring.

Hierarchy of Test Quality

In order of importance:

  1. Tests the right thing — verifies behavior that matters, not implementation details
  2. Fails when it should — catches real bugs, not just coverage theater
  3. Readable — someone can understand what's being tested without reading the implementation
  4. Maintainable — doesn't break when you refactor internals

Contract-First Approach

Before writing any test, answer:

  • What does this function promise? (not what does it do internally)
  • What inputs are valid? What happens with invalid ones?
  • What can go wrong? How should failures manifest?
  • What side effects matter? (if any)

This reframes test generation from "cover the code" to "verify the promises."

Test Structure

Use the AAA pattern:

  • Arrange — minimal setup
  • Act — one call
  • Assert — one logical assertion

Name tests clearly: Method_Scenario_ExpectedBehavior

Examples:

  • Add_EmptyString_ReturnsZero
  • Login_ExpiredToken_ThrowsAuthError
  • ParseConfig_MissingRequiredField_ReturnsValidationError

Test Quantity Guidelines

Don't overtest. A 2-function class doesn't need 20 tests.

Rules of thumb:

  • 1-3 tests per public method for straightforward logic
  • +1 test per significant branch (if/else that matters)
  • +1 test per error condition that callers need to handle
  • Edge cases only for critical inputs — not every possible null/empty/boundary

Ask: "If this test caught a bug, would I care?" If no, skip it.

Coverage theater is worse than no tests — it creates maintenance burden without catching bugs.

Edge Cases

Consider these based on input types. Don't exhaust all — focus on important inputs and critical flows:

Input Type Edge Cases to Consider
String empty "", whitespace, null/undefined
Number 0, negative, boundary values
Array/List empty [], single element
Object/Map empty {}, missing required keys
Optional/Nullable null, undefined, absent

The question: "What inputs would break this in production?" — test those.

Business Logic Patterns

State Machines

  • Test every valid transition
  • Test that invalid transitions fail/throw
  • Test initial state and terminal states

Validation Rules

  • Each rule in isolation
  • Rules in combination (do they interact?)
  • Boundary values: "just valid" and "just invalid"

Calculations

  • Known input → known output
  • Inverse operations: parse(serialize(x)) === x
  • Mathematical properties: commutativity, associativity, idempotence

What NOT to Test

  • Private methods — test through the public interface
  • Framework code — don't test that React renders or that Express routes
  • Trivial getters/setters — unless they have logic
  • Third-party libraries — trust them unless user explicitly wants to verify outputs (ask if unclear)
  • Implementation details — internal method calls, execution order (unless it's the contract)

Mocking Hierarchy

Prefer in this order:

  1. Real — use actual implementations when feasible (fast dependencies, no side effects)
  2. Fake — lightweight working implementation (in-memory DB, fake payment gateway)
  3. Mock/Stub — last resort for things you can't control (external APIs, clock, filesystem)

Mock at architectural boundaries only. If mocking your own code, you're probably testing implementation details.

Test Smells

Smell Problem
Setup > 15 lines Code under test is too coupled
Mocking own code Testing implementation, not behavior
Assertions on mock calls Testing how, not what
Magic numbers without context Intent unclear
Tests depend on order Shared mutable state
Flaky tests Time-dependent or race conditions
expect(result).toBe(true) What does true mean here?
20 tests for 2 functions Overtesting, coverage theater

Common Pitfalls

Avoid these patterns:

  • Tautological testsexpect(add(2,2)).toBe(4) when add literally does a + b. Proves nothing.
  • Testing the mock — asserting the mock returned what you told it to return
  • Happy path obsession — 5 tests for the golden path, 0 for errors
  • Copy-paste structure — same test shape applied blindly to different scenarios
  • Missing the contract — testing what code does rather than what it should do

Generation Flow

  1. Read the function signature and docstring
  2. Identify the CONTRACT (what does it promise?)
  3. List edge cases by input type (use the table, don't exhaust)
  4. List failure modes (what can go wrong?)
  5. For each behavior:
    • Write test name: Method_Scenario_ExpectedBehavior
    • Arrange: minimal setup
    • Act: one call
    • Assert: one logical assertion
  6. Review: Am I testing behavior or implementation?

Transparency About Skipped Tests

List what you're not testing — one line each, with reason:

Skipped: getUsername() (trivial getter), formatDate() (thin wrapper), API error handling (needs mocks — ask if wanted)

Keep it brief. User can ask for more if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment