Testing your Code: Ensuring Quality and Confidence

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.

E2EIntegrationUnit TestsFastest & Cheapest at the bottom

  1. Unit Tests: Test small, isolated pieces of logic (like a single function). Fast and numerous.
  2. Integration Tests: Test how different parts of the app (like a Route + Database) work together.
  3. 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/else path 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

  1. 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.
  2. Giant Tests: Putting 50 assertions in one test() block. If it fails, you won't know why. Keep tests "one-assert-ish."
  3. 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.
  4. Slow Tests: Letting your test suite take 10 minutes to run. Developers will stop running them. Use mocks to keep them fast.

Mini Exercises

  1. First Test: Write a function subtract(a, b) and a Jest test that verifies it works correctly.
  2. Edge Case: Write a test for a divide(a, b) function. What should happen if b is zero? Write a test for that error case.
  3. The Mock: Create a mock function using jest.fn(). Call it twice and assert that it was called exactly twice.
  4. Async Test: Write a test for a function that returns a Promise resolving to "Success".
  5. 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

  1. What are the three levels of the "Testing Pyramid"?
  2. What does "AAA" stand for in test writing?
  3. Why do we use "Mocks" instead of real databases in unit tests?
  4. What is the difference between toBe() and toEqual() in Jest? (Hint: Objects vs. Primitives).
  5. 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, and toContain.
  • 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.