Testing your Code: Ensuring Quality and Confidence
Writing code is only half the battle. Ensuring that code works as expected—and stays working as you add new features—is what separates professional software from fragile scripts. Automated testing allows you to catch bugs early, refactor with confidence, and provide an executable "manual" for how your application should behave.
This chapter covers the Testing Pyramid, the popular Jest framework, and the "AAA" pattern for writing clear, maintainable tests.
Why This Topic Matters
Testing is the ultimate safety net. Mastering it allows you to:
- Prevent Regressions: Ensure that a fix for "Bug A" doesn't accidentally break "Feature B."
- Simplify Refactoring: Change the internal logic of a function without fear, knowing your tests will catch any behavioral changes.
- Document Your Intent: Provide clear examples of how your code is supposed to be used.
- Improve Architecture: Code that is hard to test is usually code that is poorly designed. Testing forces you to write modular, decoupled logic.
The Testing Pyramid
Not all tests are created equal. A healthy project balances three types of tests.
- Unit Tests: Test small, isolated pieces of logic (like a single function). Fast and numerous.
- Integration Tests: Test how different parts of the app (like a Route + Database) work together.
- End-to-End (E2E): Test the entire user flow in a real browser. Slow but comprehensive.
The AAA Pattern: Arrange, Act, Assert
To keep tests readable, follow the AAA structure.
test('should add two numbers correctly', () => {
// 1. Arrange (Setup the data)
const a = 5;
const b = 10;
const expected = 15;
// 2. Act (Execute the code)
const result = add(a, b);
// 3. Assert (Check the result)
expect(result).toBe(expected);
});
Mocking: Testing in Isolation
When testing a function that sends an email or talks to a database, you don't want to actually send an email every time you run tests. Instead, you use a Mock—a "fake" version of the dependency.
const sendEmail = jest.fn(); // Create a mock function
test('should call sendEmail when order is processed', () => {
processOrder(mockOrder, sendEmail);
expect(sendEmail).toHaveBeenCalledTimes(1);
expect(sendEmail).toHaveBeenCalledWith(mockOrder.email);
});
Code Coverage
Coverage measures how much of your source code is executed by your tests.
- Statement Coverage: Did every line run?
- Branch Coverage: Did every
if/elsepath run?
Goal: Aim for high coverage (80%+), but remember that 100% coverage doesn't mean your code is bug-free—it just means every line was executed once.
Common Mistakes & Pitfalls
- Testing Implementation, Not Behavior: If your test breaks every time you rename a private variable, it's too brittle. Test what the function does, not how it does it.
- Giant Tests: Putting 50 assertions in one
test()block. If it fails, you won't know why. Keep tests "one-assert-ish." - Ignoring Error Paths: Only testing the "happy path" where everything works. You must test what happens when data is missing or the database is down.
- Slow Tests: Letting your test suite take 10 minutes to run. Developers will stop running them. Use mocks to keep them fast.
Mini Exercises
- First Test: Write a function
subtract(a, b)and a Jest test that verifies it works correctly. - Edge Case: Write a test for a
divide(a, b)function. What should happen ifbis zero? Write a test for that error case. - The Mock: Create a mock function using
jest.fn(). Call it twice and assert that it was called exactly twice. - Async Test: Write a test for a function that returns a Promise resolving to "Success".
- Refactor: Take a messy function you wrote previously and write 3 unit tests for it. Then, change the internal code of the function—did the tests still pass?
Review Questions
- What are the three levels of the "Testing Pyramid"?
- What does "AAA" stand for in test writing?
- Why do we use "Mocks" instead of real databases in unit tests?
- What is the difference between
toBe()andtoEqual()in Jest? (Hint: Objects vs. Primitives). - Why is "Branch Coverage" more important than "Line Coverage"?
Reference Checklist
- I can write a unit test using the AAA pattern.
- I understand the difference between Unit, Integration, and E2E tests.
- I can use Jest matchers like
toBe,toEqual, andtoContain. - I know how to test asynchronous code using
async/await. - I can use
jest.fn()to mock external dependencies. - I understand the importance of testing error cases.