How I Reduce Duplication in Tests

Storm Troopers from Star Wars

When writing tests for a complex bit of logic that has many test cases, I often find myself passing slight variations on input data to a method.

Here's the type of thing I am using it for, cleaned up for sharing in public. I have a function that makes a database query, which behaves in a lot of different possible ways depending on what data is returned. Since we're testing communication across an I/O boundary it makes sense to mock that so that our unit/integration tests can run quickly. So here's one sample record, as mocked for returning for the first query to be executed. This is using jest syntax for mocking, if you're not familiar with it.

const mockResponse = {
personId: 123,
dateTimeCreated: '2021-01-11 06:06',
email: 'foo@bar.com',
firstName: 'John',
lastName: 'Smith',
hasPlumbus: 1,
isRegistered: 1,
hasDonated: 1,
lifetimeDonationSumCents: 10000
};

db.query.mockImplementationOnce(() => [mockResponse]);

In reality, the objects I'm using this technique on are easily twice as long as that example.

Now, for the purposes of this discussion, let's assume that you need to test 100 different possible variations of this data handler. The naive approach is to copy and paste the mock data as-is for all 100 tests. And there's nothing inherently wrong with that approach, except that it makes your test files that much larger.

Instead, I like to use the JavaScript spread operator to limit what needs to be included in each test to only the parts that affect that test.

const mockRow = {
personId: 123,
dateTimeCreated: '2021-01-11 06:06',
email: 'foo@bar.com',
firstName: 'John',
lastName: 'Smith',
hasPlumbus: 1,
isRegistered: 1,
hasDonated: 1,
lifetimeDonationSumCents: 10000
};

it('throws for a missing email address', async () => {
const testData = {
...mockRow,
email: null
};

db.query.mockImplementationOnce(() => [testData]);

await expect(async () => app.workOnPerson(123))
.rejects.toThrow('Email is null');
});

In the highlighted lines you can see that I'm creating a new testData object, which inherits all of the properties of the mockRow object, but then overwrites the email property to null, because that's the only one that matters for the purpose of this test. This is extremely useful when you need the entire record to contain something realistic so that you're isolating the one thing you want to test, without duplicating that realistic data between every test.

Why is this important? Well, like I said, without it your test code could potentially grow at an alarming rate. For my current project, even while making heavy use of this technique, I found myself with more than 9 lines of test code (including mock data and mock modules) for every line of application code. Don't take that to mean testing is bad or wasteful, but testing well can be a lot of work and require a lot of setup data.

Webmentions

It's like comments, but you do it on Twitter.

4 Likes

Martin Mädler Ben Nadel Joℏn C. ₿l₳nd II 🤓 Gavin Pickin

1 Reply

Ben Nadel Ben Nadel
I love me some spread operator! That's one of the things I'm looking forward to now that I'm off IE11.
Add your comment: Tweet about this article.

Webmentions via webmention.io.

Discuss on TwitterEdit on GitHubContributions