DEV Community

Cover image for Using data sets in your Jest tests
Krzysztof Szala
Krzysztof Szala

Posted on • Updated on

Using data sets in your Jest tests

Data sets or data providers in testing are powerful tools, which can let you keep your test clean and simple. Checking only happy path doesn't prove your application works as expected. High-quality unit tests need to check many cases with different data. Let us consider such a case:

We hire moderators for keeping the order in our social media service. Each moderator has his own base salary, but for their action they can earn some additional penalties and bonuses. Penalties are expressed with percentage by which the salary will be reduced. While bonuses are just values which will be added to the base salary. Important business logic – the penalties are handled BEFORE bonuses, so even if moderator got 100% penalty, it can still get some money with additional bonuses. Here is the salary calculation equation:

FINAL SALARY = (BASE SALARY - PERCENTAGE OF BASE SALARY) + BONUSES
Enter fullscreen mode Exit fullscreen mode

A simple implementation of business logic described below would look like this:

class SalaryService {
  static getFinalSalary(
    baseSalary: number,
    penalties: number,
    bonuses: number
  ): number {
    return baseSalary * (1 - penalties / 100) + bonuses;
  }
}
Enter fullscreen mode Exit fullscreen mode

Ok, now is time to coverage our code with some unit tests:

describe('SalaryService', () => {
  describe('getFinalSalary', () => {
    it('returns calculated final salary', () => {
      const result = SalaryService.getFinalSalary(10, 50, 2);

      expect(result).toBe(7);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

This is a perfectly fine test, it is short and clean. But it doesn't prove that tested code fulfills business requirements because it can just always return 7. We need to check our method against more than just one case. Three different input sets will be enough for now. So, what we do with our test? Copy and paste like this?

describe('SalaryService', () => {
  describe('getFinalSalary', () => {
    it('returns calculated final salary', () => {
      const result = SalaryService.getFinalSalary(10, 50, 2);

      expect(result).toBe(7);
    });

    it('returns calculated final salary', () => {
      const result = SalaryService.getFinalSalary(0, 50, 3);

      expect(result).toBe(3);
    });

    it('returns calculated final salary', () => {
      const result = SalaryService.getFinalSalary(20, 100, 1);

      expect(result).toBe(1);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

It doesn't look good – we duplicate lots of code. And this is simple example, image if it would be something far complicated. Luckily, there is a great solution for such an issue – data sets!

Data sets or data providers allow us to rerun the same test with different sets of input values. So first we should gather our data in one consistent array:

const dataSet = [
  [10, 50, 2, 7],
  [0, 50, 3, 3],
  [20, 100, 1, 1],
];
Enter fullscreen mode Exit fullscreen mode

Then we need to rewrite our test a bit our test. Remove all duplicated code, and leave just one test. Now we pass our dataSet as argument to .each() at test implementation or test suit level. In the callback, we will receive parameters with values passed in each row of our data set:

describe('SalaryService', () => {
  describe('getFinalSalary', () => {
    const dataSet = [
      [10, 50, 2, 7],
      [0, 50, 3, 3],
      [20, 100, 1, 1],
    ];

    it.each(dataSet)('returns calculated final salary', (baseSalary, penalties, bonuses, expectedValue) => {
      const result = SalaryService.getFinalSalary(baseSalary, penalties, bonuses);

      expect(result).toBe(expectedValue);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Ok, it looks better now – we don't have code duplication anymore, and we test many cases with one more generic test. But when you look at our data set, you will probably find it quite hard to read. Without checking the callback arguments, we don't have any what each value represents. Let's fix it.

const dataSet = [
  { baseSalary: 10, penalties: 50, bonuses: 2, expectedValue: 7},
  { baseSalary: 0, penalties: 50, bonuses: 3, expectedValue: 3},
  { baseSalary: 20, penalties: 100, bonuses: 1, expectedValue: 1},
];
Enter fullscreen mode Exit fullscreen mode

As you can see, we've replaced our nested arrays with much more explicit objects. Now everyone, who looks at this data set, will understand what it contains. We need also to change the way how these values are passed to our test body. Change:

(baseSalary, penalties, bonuses, expectedValue)
Enter fullscreen mode Exit fullscreen mode

to destructuring assignment:

({ baseSalary, penalties, bonuses, expectedValue})
Enter fullscreen mode Exit fullscreen mode

You can also use data set values in test description – it can be helpful when some test won't pass. This is what our refactored test case look like. Now we can say it's data-driven test!

describe('SalaryService', () => {
  describe('getFinalSalary', () => {
    const dataSet = [
      { baseSalary: 10, penalties: 50, bonuses: 2, expectedValue: 7 },
      { baseSalary: 0, penalties: 50, bonuses: 3, expectedValue: 3 },
      { baseSalary: 20, penalties: 100, bonuses: 1, expectedValue: 1 },
    ];

    it.each(dataSet)(
      'returns calculated final salary ($baseSalary, $penalties, $bonuses)',
      ({ baseSalary, penalties, bonuses, expectedValue }) => {
        const result = SalaryService.getFinalSalary(
          baseSalary,
          penalties,
          bonuses
        );

        expect(result).toBe(expectedValue);
      }
    );
  });
});
Enter fullscreen mode Exit fullscreen mode

Now, when you get any errors related to tested method, it's going to be very easy to add another case which will cover it. Remember – always write your test against as many worthwhile cases as you can invent!

Ps. Data sets support is included in Jest since version 23. If for some reasons you're still using an older build, check jest-each npm package, which provides the same functionality.

Discussion (0)