jasmine parameterized unit test

14,853

Solution 1

Based on piotrek's answer and the article Parameterized testing in Javascript, you could also use the following approach which uses ES6 syntax:

[
  ['abc', 3],
  ['ab', 2],
  ['', 0],
].forEach(([string, expectedLength]) => {
  it(`should return length ${expectedLength} for string "${string}"`, () => {
    expect(string.length).toBe(expectedLength);
  });
});

I have tested it with the Jest test framework, but it should work with Jasmine as well.

Solution 2

Better solution (especially if you use TypeScript)

Another solution is to use Array of Objects instead of Array of Arrays. It fits better if you use some typing system like TypeScript.


Type issue

Imagine you have the following parametrised test:

it('action(value) should reset the forms pool only if value is true', () => {
  [
    [true, 1],
    [false, 0],
  ].forEach(([value, calledTimes]) => {
    spyResetFormsPool.calls.reset();

    component.action(value); // type error #1

    expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes); // type error #2
  });
});
    

with TypeScript, it will fail to compile giving two errors:

error #1:

error TS2345: Argument of type 'number | boolean' is not assignable to parameter of type 'boolean'.

error #2:

error TS2345: Argument of type 'number | boolean' is not assignable to parameter of type 'number'. Type 'true' is not assignable to type 'number'.

That is because TypeScript sees an array of 'number | boolean'.

We could quickly solve this warning by using some explicit cast:

it('action(value) should reset the forms pool only if value is true', () => {
  [
    [true, 1],
    [false, 0],
  ].forEach(([value, calledTimes]) => {
    spyResetFormsPool.calls.reset();

    component.action(value as boolean); // necessary cast

    expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes as number);  // necessary cast
  });
});

however this solution is not very nice.


Solution

A better way is to use Array of Objects, so the types are correctly handled by default and there is no need of explicit casting:

it('action(value) should reset the forms pool only if value is true', () => {
  [
    { value: true, calledTimes: 1 },
    { value: false, calledTimes: 0 },
  ].forEach(({ value, calledTimes }) => {
    spyResetFormsPool.calls.reset();

    component.action(value);

    expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes);
  });
});

Do you want to use for instead of forEach (I personally find it more readable)? That's also possible:

it('action(value) should reset the forms pool only if value is true', () => {
  for (const {value, calledTimes} of [
    {value: true, calledTimes: 1},
    {value: false, calledTimes: 0},
  ]) {
    spyResetFormsPool.calls.reset();

    component.action(value);

    expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes);
  }
});

Alternatively, you can also move the it inside the loop. When I do this, I usually add a testId to each object so I can keep track of which tests are failing:

for (const {value, calledTimes} of [
  { testId: 1, value: true,  calledTimes: 1 },
  { testId: 2, value: false, calledTimes: 0 },
]) {
  it(`action(value) should reset the forms pool only if value is true [${testId}]`, () => {
    spyResetFormsPool.calls.reset();

    component.action(value);

    expect(spyResetFormsPool).toHaveBeenCalledTimes(calledTimes);
  });
}

Solution 3

You can use the following convention to increase readability:

const testCases = [
    {actualValue: true, expectedValue: true},
    {actualValue: false, expectedValue: false}
]

testCases.forEach(({actualValue, expectedValue}) => {
    it(`should be the same given: ${actualValue} and expected :${expectedValue} values`, () => {
      expect(actualValue).toBe(expectedValue)
    })
})

You'll see the following test cases to run:

Test Results
+ should be the same given: true and expected: true values
+ should be the same given: false and expected: false values

Solution 4

i haven't worked with jasmine since a long time but it was pretty easy to add parameterized tests:

  ['abc', 3,
   'ab',  4,
   '',    0].
it('should contain string length', function(string, expected){
   expect(string.length).toBe(expected);
});

with just a few lines of infrastructure code:

Array.prototype.it = function(description, testCaseFunction) {
     _(this)
      .chunk(testCaseFunction.length)
      .each(function(innerArray){
                it(description + ' ' + JSON.stringify(innerArray), function(){
                   testCaseFunction.apply(this, innerArray);
                });     
      })
      .value();
};

depending on your desired syntax and willingness to change default js objects, you have plenty of options: http://blog.piotrturski.net/2015/04/jasmine-parameterized-tests.html

Share:
14,853
Ashley Kilgour
Author by

Ashley Kilgour

C# coder, believes in TDD, and having unit tests to make sure I haven't broke anything.

Updated on June 26, 2022

Comments

  • Ashley Kilgour
    Ashley Kilgour about 2 years

    Okay as a C# NUnit guy this might be odd.

    But does jasmine allow parameterized unit test?

    I am not sure if it goes against the "declare" and "it" to make things readable to non programmers.

    I have seen some third party plug ins but they are kind of old, not sure if it has been added to jasmine. If I am ment to use a plug in

    Just to help anyone who finds this in the future, I have been told on jasmine forum There is no first class support for parameterized tests within Jasmine itself.

  • Astridax
    Astridax almost 5 years
    It ought to be added that if you have a beforeEach block and possibly an afterEach block this entirely breaks. If you try and set up a test this way with a beforeEach block Jest throws a 'TypeError: Cannot read property '[object Array]' of undefined' exception. However all is not lost, it seems to work if you assign the test cases to a variable and iterate over that, as per Dymtro's answer stackoverflow.com/a/55909737/1392407