Write Cleaner Unit Tests Using Parameterization

If you’re used to writing unit tests, you might already know that when you’re testing a function with parameters, you want to test it using many (or ideally, all) possible inputs. That is to ensure that the function is doing what we want for anything we decide to throw at it. However, if we’re repeating the same pattern of set up data – run function – expect certain result for writing unit tests, an undesirable pattern could potentially emerge.

In a Testing Rut

Let’s check out an example of this using a Javascript unit testing framework called Vitest*. As a trivial example, let’s test that a function can successfully add any two integers.

*note: Vitest is a unit testing framework used with the JS build tool Vite. It was created to be almost completely similar in syntax to Jest, so most (if not all) of this code will work for both frameworks.


describe("Add any two integers", () => {
	it("can add zero", () => {
		const a = 0;
		const b = 0;
		const result = addition(a, b);
		const expected = 0;
		expect(result).toBe(expected);
	});

	it("can add positive integers", () => {
		const a = 1;
		const b = 3;
		const result = addition(a, b);
		const expected = 4;
		expect(result).toBe(expected);
	});

	it("can add negative integers", () => {
		const a = -1;
		const b = -3;
		const result = addition(a, b);
		const expected = -4;
		expect(result).toBe(expected);
	});

	it("can add positive and negative integers", () => {
		const a = -1;
		const b = 3;
		const result = addition(a, b);
		const expected = 2;
		expect(result).toBe(expected);
	});
});

These tests work, and if the function has been implemented correctly, they will pass. However, this pattern is not very DRY (Don’t Repeat Yourself). We follow the same pattern over and over for each test. We declare test parameter variables and execute the function, expecting a result, copy, and paste. Wouldn’t it be great if we had an easy way to iterate over the same test code block with different parameter variables?

Enter Parameterization

Good news, there is! With parameterized tests, we can execute the same block of test code over multiple parameter variables.

Let’s look at the same example with our test data inputs parameterized. In Vitest/Jest, we can do this by using “it.each()”.


describe("Add any two integers with parameterized tests", () => {
	it.each([
		[0, 0, 0],
		[1, 3, 4],
		[-1, -3, -4],
		[-1, 3, 2],
	])("Can add two integers", (a, b, expected) => {
		const result = addition(a, b);
		expect(result).toBe(expected);
	});
});

Boom. We’re giving our test block an array of test data. That data is inserted into each test block iteration through the corresponding parameter variables. Isn’t that so much cleaner?

You might still be wondering, “But what about our neat and readable test messages?” We can certainly make this parameterized test a lot more readable and informative when we use objects as our parameters. Check this out: 


describe("Add any two integers with readable parameterized tests", () => {
	it.each([
		{
			message: "Adding zero",
			a: 0,
			b: 0,
			expected: 0,
		},
		{
			message: "Adding positive integers",
			a: 1,
			b: 3,
			expected: 4,
		},
		{
			message: "Adding negative integers",
			a: -1,
			b: -3,
			expected: -4,
		},
		{
			message: "Adding a positive and negative integer",
			a: -1,
			b: 3,
			expected: 2,
		},
	])("$message: $a plus $b should be $expected", ({ a, b, expected }) => {
		const result = addition(a, b);
		expect(result).toBe(expected);
	});
});

When we use an object to format our parameter data, our parameterized test becomes a lot more readable. Vitest also allows us to inject our object properties into our test message using the “$propertyKey” syntax. This gives us better feedback if one of our iterations is failing. Here, we even added a message property to declare a neat and specific purpose for each dataset. So, when we run the whole test suite we’ve created this far (with a purposely failing test), we get the following:

Parameterization test

Cleaner Unit Tests Using Parameterization

As you can see, we can use parameterization in our unit tests to create some clean, readable, and informative tests. In Vitest/Jest, these features can go even further. We can use “.each” in our “describe” blocks in order to parameterize an entire test suite or potentially double nest test parameter blocks. Or, we can even add a function to each parameter dataset, which, for example, could allow us to set up test data differently on each iteration.

Even if you’re not testing JavaScript functions using Vitest or Jest, most other unit-testing frameworks in other languages support parameterization as well. Look out for those repeated patterns in your unit tests. You might be able to implement some parameterization to make them more succinct.

Conversation

Join the conversation

Your email address will not be published. Required fields are marked *