Posts

Unit Testing: Rules and Best Practices

Rules to Follow FIRST Principles Fast – Tests should run quickly to allow frequent execution. Independent – Tests must not depend on each other. Repeatable – A test should always yield the same result. Self-Checking – …

February 21, 2025 3 min read 584 words

In this article

Rules to Follow

FIRST Principles

  1. Fast – Tests should run quickly to allow frequent execution.
  2. Independent – Tests must not depend on each other.
  3. Repeatable – A test should always yield the same result.
  4. Self-Checking – Tests should automatically verify correctness.
  5. Timely – Write tests before or alongside implementation.

Coverage & Quality

  • Target 85%+ meaningful test coverage, prioritizing critical paths.
  • Cover happy paths, edge cases, and failure scenarios.
  • Async tests should be properly awaited or return promises.
  • Mock dependencies to isolate the unit under test.
  • Ensure all branches and conditions are tested.

Self-Describing Tests

  • Descriptive Namingit('should return X when Y happens').
  • Truth Tables (it.each) – Cover multiple cases in one statement.
  • Mock Naming Convention – Differentiate real and mock objects.
  • describe Blocks – Group related tests logically.
  • Named Constants – Avoid magic values in test cases.

SEE Pattern (made this one up)

  1. Setup – Initialize inputs and dependencies.
  2. Execute – Run the function under test.
  3. Expect – Assert the expected outcome.

NOTE: I call it “SEE” because you see immediately what it did or didn’t do.

Frameworks & Tools

  • Use Jest, Mocha, JUnit, NUnit, xUnit, or equivalent.
  • Leverage test runners, assertions, and mocks.
  • Structure tests with beforeEach, afterEach, beforeAll, afterAll.
  • Use assertion libraries like Chai, Sinon, Jasmine, etc..

Things to Avoid

Test Bleeds

  • Reset mocks and test data before/after each test.
  • Ensure immutability of test data.
  • Do not pass values between tests.

External Dependencies

  • No reliance on databases, APIs, or file systems.
  • Use mocks, stubs, or in-memory implementations.

Complex Test Logic

  • No loops or conditionals in tests.
  • Keep tests short and clear.

Behavioral Assertions Matter

  • Assert not just the result, but how many times methods are called.
  • Prevent accidental excessive calls causing performance issues.

Private/Internal Method Testing

  • Test public interfaces only.
  • If necessary, test private methods via subclassing or bracket notation.
  • Consider refactoring if testing private methods directly.

Flaky Tests Are a No-Go

  • Flaky is for pastries not tests.
  • Tests should be reliable and deterministic.
  • Flaky tests erode confidence and slow development.

How to Structure Tests

describe("Math", () => {
  describe("add", () => {
    it("should return the sum of two numbers", () => {
      expect(Math.add(1, 2)).toBe(3);
    });
  });
});
Math (describe)
├── add (describe)
│   ├── (it) should return the sum of two numbers

Mocking Dependencies

// Setup
const mockLogger = { log: jest.fn() };
// Execute
new Service(mockLogger).execute();
// Expect
expect(mockLogger.log).toHaveBeenCalledTimes(1);
expect(mockLogger.log).toHaveBeenNthCalledWith(
  1,
  "dear diary... naming is hard"
);

Edge Cases Matter

it.each([
  { a: 0, b: 0, expected: 0 },
  { a: -1, b: -1, expected: -2 },
  { a: 1, b: Spanish.INQUISITION, expected: 0 },
  { a: Number.MAX_VALUE, b: 1, expected: Number.MAX_VALUE + 1 },
])("should return $expected when adding $a and $b", ({ a, b, expected }) =>
  expect(add(a, b)).toBe(expected)
);

Ensuring Quality & Maintainability

Automate Testing

  • Integrate tests into CI/CD pipelines.
  • Run tests before merging to catch regressions early.

Monitor & Improve Coverage

  • Use coverage reports to find gaps.
  • Publish coverage badges in READMEs for visibility.
  • Set coverage thresholds for new features consistently.
  • Continuously update the minimum required code coverage to match the current levels, so every improvement becomes a new baseline for further progress
  • Focus on critical paths and high-risk areas.

Refactor Tests as Code Evolves

  • Keep tests aligned with code changes.
  • Remove redundant or obsolete tests.

Review Tests Like Production Code

  • Ensure clean, readable, and maintainable tests.
  • Peer review test cases to maintain quality.

By following these principles, unit tests will be reliable, efficient, and maintainable, elevating software quality and developer confidence.