DEV Community

Cover image for Simplifying jest stubs using jest-when
Manuel Rivero
Manuel Rivero

Posted on • Originally published at codesai.com

Simplifying jest stubs using jest-when

In a recent deliberate practice session with some developers from Audiense (with whom we’re doing the Codesai’s Practice Program twice a month), we were solving the Unusual Spending Kata
in JavaScript.

While test-driving the UnusualSpendingDetector class we found that writing stubs using plain jest can be a bit hard. The reason is that jest does not match mocked function arguments, so to create stubbed responses for particular values of the arguments we are forced to introduce logic in the tests.

Have a look at a fragment of the tests we wrote for UnusualSpendingDetector class:

import {UnusualSpendingDetector} from "../src/UnusualSpendingDetector";

describe('UnusualSpendingDetector', () => {
  const currentMonth = '2020-02';
  const previousMonth = '2020-01';
  const userId = '1234';
  let paymentsRepository;
  let detector;

  beforeEach(() => {
    const calendar = {
      getCurrentMonth: () => currentMonth,
      getPreviousMonth: () => previousMonth,
    };
    paymentsRepository = {
      find: jest.fn()
    }
    detector = new UnusualSpendingDetector(paymentsRepository, calendar);
  });

  // more tests ...

  test(
    'detects an unusual spending when spending for a category in consecutive months grew 50% or more', 
    () => {
      const currentMonthPayments = [payment('food', 200, currentMonth)];
      const previousMonthPayments = [payment('food', 100, previousMonth)];
      paymentsRepositoryWillReturn(currentMonthPayments, previousMonthPayments);

      const unusualSpendings = detector.detect(userId);

      expect(unusualSpendings).toEqual([unusualSpending('food', 200)]);
  });

  // more tests ...

  function payment(category, amount, month) {
    return {category, amount, month};
  }

  function unusualSpending(category, amount) {
    return {category, amount};
  }

  function paymentsRepositoryWillReturn(currentMonthPayments, previousMonthPayments) {
    paymentsRepository.find.mockImplementation((userId, month) => {
      if (month === currentMonth) {
        return currentMonthPayments;
      } else if (month === previousMonth) {
        return previousMonthPayments;
      }
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Notice the paymentsRepositoryWillReturn helper function, we extracted it to remove duplication in the tests. In this function we had to add explicit checks to see whether the arguments of a call match some given values. It reads more or less ok, but we were not happy with the result because we were adding logic in test code (see all the test cases).

Creating these stubs can be greatly simplified using a library called jest-when which helps to write stubs for specifically matched mocked function arguments. Have a look at the same fragment of the tests we wrote for UnusualSpendingDetector class now using jest-when:

import {UnusualSpendingDetector} from "../src/UnusualSpendingDetector";
import {when} from 'jest-when';

describe('UnusualSpendingDetector', () => {
  const currentMonth = '2020-02';
  const previousMonth = '2020-01';
  const userId = '1234';
  let paymentsRepository;
  let detector;

  beforeEach(() => {
    const calendar = {
      getCurrentMonth: () => currentMonth,
      getPreviousMonth: () => previousMonth,
    };
    paymentsRepository = {
      find: jest.fn()
    }
    detector = new UnusualSpendingDetector(paymentsRepository, calendar);
  });

  // more tests ...

  test('detects an unusual spending when spending for a category in consecutive months grew 50% or more', () => {
    when(paymentsRepository.find).calledWith(userId, currentMonth)
      .mockReturnValue([payment('food', 200, currentMonth)]);
    when(paymentsRepository.find).calledWith(userId, previousMonth)
      .mockReturnValue([payment('food', 100, previousMonth)]);

    const unusualSpendings = detector.detect(userId);

    expect(unusualSpendings).toEqual([unusualSpending('food', 200)]);
  });

  // more tests ...

  function payment(category, amount, month) {
    return {category, amount, month};
  }

  function unusualSpending(category, amount) {
    return {category, amount};
  }
});
Enter fullscreen mode Exit fullscreen mode

Notice how the paymentsRepositoryWillReturn helper function is not needed anymore, and how the fluent interface of jest-when feels nearly like canonical jest syntax.

We think that using jest-when is less error prone than having to add logic to write your own stubs with plain jest, and it’s as readable as or more than using only jest (see all the refactored test cases).

To improve readability we played a bit with a functional builder. This is the same fragment of the tests we wrote for UnusualSpendingDetector class now using a functional builder over jest-when:

import {UnusualSpendingDetector} from "../src/UnusualSpendingDetector";
import {stub} from "./PaymentsRepositoryHelper";

describe('UnusualSpendingDetector', () => {
  const currentMonth = '2020-02';
  const previousMonth = '2020-01';
  const userId = '1234';
  let paymentsRepository;
  let detector;
  let knowingThat;

  beforeEach(() => {
      paymentsRepository = {
        find: jest.fn()
      };
      knowingThat = stub(paymentsRepository);
      const calendar = {
        getCurrentMonth: () => currentMonth,
        getPreviousMonth: () => previousMonth,
      };
      detector = new UnusualSpendingDetector(paymentsRepository, calendar);
    }
  );

  // more tests ...

  test(
    'detects an unusual spending when spending for a category in consecutive months grew 50% or more', 
    () => {
      knowingThat().inMonth(currentMonth).userWith(userId).hasPaid(
        [payment('food', 200, currentMonth)])
        .andThat().inMonth(previousMonth).userWith(userId).hasPaid(
        [payment('food', 100, previousMonth)]);

      const unusualSpendings = detector.detect(userId);

      expect(unusualSpendings).toEqual([unusualSpending('food', 200)]);
  });

  // more tests ...

  function payment(category, amount, month) {
    return {category, amount, month};
  }

  function unusualSpending(category, amount) {
    return {category, amount};
  }
});
Enter fullscreen mode Exit fullscreen mode

where the PaymentsRepositoryHelper is as follows:

import {when} from "jest-when";

export function stub(paymentsRepository) {
  const monthPaymentsBuilder = {
    inMonth: inMonth
  };

  return knowingThat;

  function knowingThat() {
    return monthPaymentsBuilder;
  }

  function inMonth(month) {
    return {
      userWith: (userId) => {
        return {
          hasPaid: (payments) => {
            when(paymentsRepository.find).calledWith(userId, month)
              .mockReturnValue(payments);
            return {
              andThat: knowingThat
            };
          }
        };
      }
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

In this last version we were just playing a bit with the code in order to find a way to configure the stubs both in terms of the domain and without free variables (have a look at all the test cases using this builder). In any case, we think that the previous version using jest-when was already good enough.

Probably you already knew jest-when, if not, give it a try. We think it can help you to write simpler stubs if you're using jest.

Top comments (0)