Skip to main content
Multiple test input combinations with jest

Multiple test input combinations with jest

·670 words·4 mins·

One challenge that I’ve been facing recently is how to write readable jest tests that exhaustively test my functions with all possible input combinations. On early stages this is usually simple, but as the functions evolve and get more input parameters, the test code starts to get bloated and confusing even to the person that wrote it (yours truly).

A practical example
#

Let’s imagine that we have the following callback computeIngredients(cake, vegan): Ingredient[] which returns the list of ingredients required to make a cake, supporting both vegan and non-vegan recipes. We want to implement a business rule that requires PeanutButter to always be in the list of ingredients regardless of the cake or whether its vegan. Given that we only support ChocolateCake and OrangeCake for now, it would be fairly simple to cover all test cases:

describe("Compute Ingredients", () => {
  test.each`
    cake                   | vegan
    ${Cakes.ChocolateCake} | ${true}
    ${Cakes.ChocolateCake} | ${false}
    ${Cakes.OrangeCake}    | ${true}
    ${Cakes.OrangeCake}    | ${false}
  `(
    `Should have peanut butter cake=${cake}, and vegan=${vegan}`,
    ({ cake, vegan }) => {
      const result = computeIngredients(cake, vegan);
      expect(result).toContainEqual(Ingredients.PeanutButter);
    }
  );
});

If we add another cake recipe VanillaCake and glutenFree to the modifiers, the number of entries in our test.each table goes to triple, making the test list of test cases much bigger and hard to understand.

test.each`
  cake                   | vegan    | glutenFree
  ${Cakes.ChocolateCake} | ${true}  | ${true}
  ${Cakes.ChocolateCake} | ${true}  | ${false}
  ${Cakes.ChocolateCake} | ${false} | ${true}
  ${Cakes.ChocolateCake} | ${false} | ${false}
  ${Cakes.OrangeCake}    | ${true}  | ${true}
  ${Cakes.OrangeCake}    | ${true}  | ${false}
  ${Cakes.OrangeCake}    | ${false} | ${true}
  ${Cakes.OrangeCake}    | ${false} | ${false}
  ${Cakes.MangoCake}     | ${true}  | ${true}
  ${Cakes.MangoCake}     | ${true}  | ${false}
  ${Cakes.MangoCake}     | ${false} | ${true}
  ${Cakes.MangoCake}     | ${false} | ${false}
`;

Exploiting describe.each
#

A simple solution if you really need to generate exhaustive combinations for your test case would be to exploit describe.each by using it to loop over each argument value multiple times. Each describe creates a new scope, and we can nest as many describe blocks as we like. Using each in combination with describe allows us to achieve a similar behavior to nested for loops

describe("Compute ingredients", () => {
  // Repeats tests for each cake
  describe.each(Object.values(Cakes))("When cake = %s", (cake) => {
    // Repeats tests for vegan/non vegan
    describe.each([true, false])("And vegan = %s", (vegan) => {
      // Repeats tests for gluten free/non gluten free
      describe.each([true, false])("And glutenFree = %s", (glutenFree) => {
        test("Should have peanut butter", () => {
          // Do the actual test
          const result = computeIngredients(cake, vegan, glutenFree);
          expect(result).toContainEqual(Ingredients.PeanutButter);
        });
      });
    });
  });
});

Where the results would be something like this

Compute ingredients
    When cake = OrangeCake
      And vegan = true
        And glutenFree = true
          ✓ Should have peanut butter (3 ms)
        And glutenFree = false
          ✓ Should have peanut butter
      And vegan = false
        And glutenFree = true
          ✓ Should have peanut butter
        And glutenFree = false
          ✓ Should have peanut butter
    When cake = ChocolateCake
      And vegan = true
        And glutenFree = true
          ✓ Should have peanut butter
        And glutenFree = false
          ✓ Should have peanut butter
      And vegan = false
        And glutenFree = true
          ✓ Should have peanut butter (1 ms)
        And glutenFree = false
          ✓ Should have peanut butter (1 ms)
    When cake = MangoCake
      And vegan = true
        And glutenFree = true
          ✓ Should have peanut butter (1 ms)
        And glutenFree = false
          ✓ Should have peanut butter (1 ms)
      And vegan = false
        And glutenFree = true
          ✓ Should have peanut butter
        And glutenFree = false
          ✓ Should have peanut butter (1 ms)

Conclusion
#

We should always try to keep our tests as simple as possible, easier to read and to maintain. Nesting describe.each scopes provides a simple way to generate the highest possible number of argument combinations and test our code against all of them, giving us more confidence to release. On the other hand, abusing this functionality might also mean that our interfaces are too complex or not well-structured and the wisest thing to do could be to rethink them.

Author
David Simão
Software Engineer