Parametrize Angular Jasmine Tests

Functions that map a value from one form to another are useful since you can separate the mapping logic from things like services and components which are best kept as simple as possible. This allows for easy tests where you can check a range of conditions which might take a lot more boilerplate code if the logic was embedded in a component or service. It also allows you to re-use the mapping.

This does lead to simpler test cases, but because they are so simple the test boilerplate can become repetitive and overwhelm what is actually being tested. In practice, these types of tests are often copied and pasted which can lead to more maintenance overhead or outdated commentary around tests. To illustrate, consider the following mapping function.

Transformating Function

The following function takes in a number and returns numbers based on a series of conditional checks.
transform.ts

// transform.ts
export function transform(value: number): number {
  if (value === undefined) {
    return 0;
  }
  if (value === null) {
    return 0;
  }
  if (value < 0) {
    return -1;
  }
  if (value === 0) {
    return 0;
  }
  return 1;
}

Traditional Tests

To test the function you might come up with cases where the input is undefined, null, -10, -1, 0, 1 and 10. Those seem reasonable given the checks and boundaries in the tests. This would result in something like the following test file.
transform.spec.ts

// transform.spec.ts
import { transform } from './transform';

describe('transform', () => {
  describe('input value is undefined', () => {
    it('should return 0', () => {
      expect(transform(undefined)).toEqual(0);
    });
  });

  describe('input value is null', () => {
    it('should return 0', () => {
      expect(transform(null)).toEqual(0);
    });
  });

  describe('input value is -10', () => {
    it('should return -1', () => {
      expect(transform(-10)).toEqual(-1);
    });
  });

  describe('input value is -1', () => {
    it('should return -1', () => {
      expect(transform(-1)).toEqual(-1);
    });
  });

  describe('input value is 0', () => {
    it('should return 0', () => {
      expect(transform(0)).toEqual(0);
    });
  });

  describe('input value is 10', () => {
    it('should return 1', () => {
      expect(transform(10)).toEqual(1);
    });
  });

  describe('input value is 1', () => {
    it('should return 1', () => {
      expect(transform(1)).toEqual(1);
    });
  });
});

The key pieces of information in the tests is that undefined and null map to 0, negative numbers map to -1, 0 maps to 0 and positive numbers map to 1. Compare that simple sentence to 43 lines of code required to implement the tests.

Parametrized Tests

The following shows how the 43 lines of test code can be reduced to 18.
transform.parametrized.spec.ts

// transform.parametrized.spec.ts
import { transform } from './transform';

describe('transform parametrized tests', () => {
  // Defining input value and expected transformation
  [
    { inputValue: undefined, expectedValue: 0 },
    { inputValue: null, expectedValue: 0 },
    { inputValue: -10, expectedValue: -1 },
    { inputValue: -1, expectedValue: -1 },
    { inputValue: 0, expectedValue: 0 },
    { inputValue: 1, expectedValue: 1 },
    { inputValue: 10, expectedValue: 1 }
  ].forEach(({inputValue, expectedValue}) => {
    describe(`input is ${inputValue}`, () => {
      it(`should return ${expectedValue}`, () => {
        expect(transform(inputValue)).toEqual(expectedValue);
      });
    });
  });
});

Lines 7 to 13 clearly demonstrate the expected input to output mapping. Lines 15-19 demonstrate how the mapping is achieved. The forEach loop takes advantage of object destructing to assign an object from the array to parameters of the testing function on line 14. The describe and it functions are passed arguments that include the input and expected value, which makes reading and understanding the output of failed tests easier, on lines 15 and 16, respectively.

There are drawbacks to parametrizing tests and, in a lot of cases, can make tests confusing. In this case, the parameters and code to execute the test fit on the same page in a code editor, which makes it easy to understand. There are also few input parameters and the test logic is simple.

When the number of input parameters gets large, it makes the test harder to read because you have to jump back and forth between the parameter definition and the test code. If the test code is longer and complex, introducing parameters increases the complexity of the test even further. If parametrizing the test means you have to significantly increase the logic of the test, it means the test is hard to understand when you come back to it a few months down the track.

However, with few input parameters and a function for which little test logic is required, using parametrised tests reduces boilerplate code and makes the tests easier to understand. TypeScript also helps as it will infer the types of the input and expected parameters for you.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s