Skip to Content
Course content

121: Unit Testing with Jest

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time with developers moving into professional environments: the belief that unit testing is essentially just a way to automate the "manual check" you already do in the browser. You write your function, you run the app, you see it works, and then you write a test to "prove" it works. This is a fundamental misunderstanding of why we use tools like Jest.

Testing isn't just a double-check for your manual testing

If you only write tests for the "happy path" that you've already manually verified, you're not actually testing; you're just documenting that the code worked once. The real power of unit testing is regression testing—ensuring that a change you make to a utility function in six months doesn't silently break an edge case you forgot existed.

Take this common scenario. Imagine you have a function that calculates a user's membership discount. It seems simple enough:

function calculateDiscount(price, code) {
  if (code === 'SUMMER10') return price * 0.9;
  if (code === 'WINTER20') return price * 0.8;
  return price;
}

You manually test 'SUMMER10', it works. You're happy. But later, a teammate changes the logic to support case-insensitive codes. They think they've fixed it, but they've actually introduced a bug where null codes now throw an error. Without a suite of tests covering those edge cases, you won't find that bug until a customer's checkout crashes in production. I've been there, and it's a nightmare.

Defining the contract with Matchers

In Jest, we don't just "run the code." We define a contract. We use describe blocks to group related tests and test (or it) to define specific behaviors. The magic happens in the expect statement, where we use "matchers" to assert the outcome.

Instead of just checking if the discount works, we write a suite that covers the "contract" of the function:

describe('calculateDiscount', () => {
  test('applies 10% discount for SUMMER10', () => {
    expect(calculateDiscount(100, 'SUMMER10')).toBe(90);
  });

  test('applies 20% discount for WINTER20', () => {
    expect(calculateDiscount(100, 'WINTER20')).toBe(80);
  });

  test('returns original price for invalid codes', () => {
    expect(calculateDiscount(100, 'BOGUS')).toBe(100);
  });

  test('handles null or undefined codes gracefully', () => {
    expect(calculateDiscount(100, null)).toBe(100);
    expect(calculateDiscount(100, undefined)).toBe(100);
  });
});

Now, if someone breaks the null handling, the test suite fails immediately. You've shifted the burden of remembering edge cases from your brain to the machine.

Isolating logic from side effects with Mocks

The biggest hurdle I see learners hit is when their function depends on something "outside"—like an API call or a database. They think, "I can't unit test this because I don't want to actually call the API every time I run my tests."

You're right, you shouldn't. That's why we use Mocks. A unit test should test the logic of your function, not the reliability of your network. If your function fetches a user and then formats their name, you don't test the network request; you mock the response and test the formatting logic.

Using jest.mock() or jest.fn(), you can replace a real module with a fake one that returns a predictable value. This keeps your tests fast and deterministic. If your tests depend on a live server, they aren't unit tests—they're integration tests, and they'll be the first things to flake out and slow down your CI/CD pipeline.




📋 Practical Task

The Global Shipping Rate Calculator Regression Suite

You are maintaining a shipping module for an e-commerce site. The core logic is handled by a function called calculateShipping, but it relies on an external module currencyConverter.js to handle exchange rates. Your goal is to ensure the shipping logic remains intact even if the currency converter is unavailable or changes.

The Code:

// shipping.js
import { getExchangeRate } from './currencyConverter';

export function calculateShipping(weight, destination) {
  const baseRate = destination === 'Domestic' ? 5 : 20;
  const weightSurcharge = weight * 1.5;
  const total = baseRate + weightSurcharge;

  // Convert the total based on the destination's currency rate
  const rate = getExchangeRate(destination); 
  return total * rate;
}

Your Task:

  1. Write a Jest test file for calculateShipping.
  2. Use jest.mock('./currencyConverter') to prevent real API calls.
  3. Create a test case for a Domestic shipment (Weight: 2kg, Expected Rate: 1) to ensure the base rate and surcharge are calculated correctly.
  4. Create a test case for an International shipment (Weight: 2kg) where you mock getExchangeRate to return 1.2.
  5. Create an edge case test for zero weight to ensure it doesn't crash and returns only the base rate multiplied by the exchange rate.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.