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 – Tests should automatically verify correctness.
- 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 Naming –
it('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)
- Setup – Initialize inputs and dependencies.
- Execute – Run the function under test.
- 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
Group Related 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.