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;
}
});
}
});
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};
}
});
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};
}
});
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
};
}
};
}
};
}
}
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)