What Is a Unit Test?
A unit test verifies that a single, isolated piece of code — typically a function or method — behaves as expected given a specific input. The key word here is isolated: a unit test should not depend on databases, network calls, or external services. If it does, it's an integration test, not a unit test.
Unit tests are the foundation of any healthy test suite. They run fast, pinpoint failures precisely, and give you confidence to refactor without fear.
The Anatomy of a Good Unit Test
Every well-written unit test follows the AAA pattern:
- Arrange — Set up the inputs, dependencies, and expected outputs.
- Act — Call the function or method under test.
- Assert — Verify the result matches what you expected.
This three-step structure keeps tests readable and maintainable. When a test fails, anyone on the team can immediately understand what went wrong.
What Makes a Unit Test "Good"?
Not all unit tests are created equal. A high-quality unit test should be:
- Fast — Should complete in milliseconds, not seconds.
- Isolated — Doesn't rely on shared state or external systems.
- Repeatable — Produces the same result every single time it runs.
- Self-describing — The test name explains exactly what behavior is being verified.
- Single-purpose — Tests one thing at a time. Multiple assertions are fine if they all verify the same behavior.
What Should You Test?
A common question for beginners is: "Do I need to test everything?" The practical answer is no — focus your energy on code that contains logic. Specifically, prioritize testing:
- Functions with branching logic (if/else, switch statements)
- Edge cases: empty inputs, null values, boundary conditions
- Error handling and exception paths
- Complex calculations or transformations
- Public API surfaces of your modules
You generally don't need to unit test simple getters/setters, framework boilerplate, or third-party library internals.
Common Pitfalls to Avoid
Testing Implementation, Not Behavior
If your test breaks every time you refactor internal details without changing the observable behavior, it's testing how code works rather than what it does. Test outcomes, not internal steps.
Writing Tests That Always Pass
A test that never fails is useless. Always verify that your test actually catches the bug it's supposed to catch by temporarily breaking the code and confirming the test turns red.
Over-Mocking
Mocks are powerful but dangerous when overused. If you mock everything, you end up testing the mocks themselves rather than real behavior. Use mocks only to eliminate genuine external dependencies.
A Quick Example
Here's a simple JavaScript example using Jest to illustrate the AAA pattern:
// The function under test
function add(a, b) {
return a + b;
}
// The unit test
test('add returns the sum of two numbers', () => {
// Arrange
const a = 3, b = 7;
// Act
const result = add(a, b);
// Assert
expect(result).toBe(10);
});
Next Steps
Once you're comfortable with the basics, explore test-driven development (TDD) — writing tests before the implementation. It changes how you design code, often resulting in smaller, more focused functions that are naturally easier to test.
Pick one function in your current codebase today and write a unit test for it. That's all it takes to start building the habit.