199

I have a function I'd like to test which calls an external API method twice, using different parameters. I'd like to mock this external API out with a Jasmine spy, and return different things based on the parameters. Is there any way to do this in Jasmine? The best I can come up with is a hack using andCallFake:

var functionToTest = function() {
  var userName = externalApi.get('abc');
  var userId = externalApi.get('123');
};


describe('my fn', function() {
  it('gets user name and ID', function() {
    spyOn(externalApi, 'get').andCallFake(function(myParam) {
      if (myParam == 'abc') {
        return 'Jane';
      } else if (myParam == '123') {
        return 98765;
      }
    });
  });
});
Jmr
  • 12,078
  • 4
  • 39
  • 33

3 Answers3

307

In Jasmine versions 3.0 and above you can use withArgs

describe('my fn', function() {
  it('gets user name and ID', function() {
    spyOn(externalApi, 'get')
      .withArgs('abc').and.returnValue('Jane')
      .withArgs('123').and.returnValue(98765);
  });
});

For Jasmine versions earlier than 3.0 callFake is the right way to go, but you can simplify it using an object to hold the return values

describe('my fn', function() {
  var params = {
    'abc': 'Jane', 
    '123': 98765
  }

  it('gets user name and ID', function() {
    spyOn(externalApi, 'get').and.callFake(function(myParam) {
     return params[myParam]
    });
  });
});

Depending on the version of Jasmine, the syntax is slightly different:

  • 1.3.1: .andCallFake(fn)
  • 2.0: .and.callFake(fn)

Resources:

Jason McTaggart
  • 878
  • 1
  • 7
  • 18
Andreas Köberle
  • 106,652
  • 57
  • 273
  • 297
  • 14
    This is now `and.callFake` - http://jasmine.github.io/2.2/introduction.html#section-Spies:_and.callFake – Lucy Bain Apr 04 '15 at 06:44
  • 1
    I had to return different promises, so the return looked slightly different: return q.when(params[myParam]);. Otherwise, this was a spot on solution to my problem. My dream solution would be to change "and.returnValue" calls. – Bill Turner May 05 '15 at 16:31
  • 9
    feels like jasmine should have a better way of declaring this. Like `spyOn(fake, 'method').withArgs('abc').and.returnValue('Jane')` and `spyOn(fake, 'method').withArgs('123').and.returnValue(98765)`. – jrharshath Jan 14 '16 at 00:15
  • @jrharshath `.withArgs` is not working for me in jasmine 2.0 – hemkaran_raghav Apr 05 '16 at 11:59
  • 1
    `.withArgs` is not really available - I meant that such a method would make sense when writing tests. – jrharshath Apr 07 '16 at 07:18
  • @jrhashath I'm not really sure if that would be much better. With the call fake your can do the same thing with conditional logic – Embattled Swag May 07 '17 at 17:16
  • See here for Jasmine contributors comments on this: https://github.com/jasmine/jasmine/issues/32 (Helped me understand what needed to be done in code) – ryanwebjackson Nov 15 '17 at 16:37
  • Jasmine 3.0 add support for this. – Jason McTaggart Jul 17 '18 at 17:27
13

You could also use $provide to create a spy. And mock using and.returnValues instead of and.returnValue to pass in parameterised data.

As per Jasmine docs: By chaining the spy with and.returnValues, all calls to the function will return specific values in order until it reaches the end of the return values list, at which point it will return undefined for all subsequent calls.

describe('my fn', () => {
    beforeEach(module($provide => {
        $provide.value('externalApi', jasmine.createSpyObj('externalApi', ['get']));
    }));

        it('get userName and Id', inject((externalApi) => {
            // Given
            externalApi.get.and.returnValues('abc','123');

            // When
            //insert your condition

            // Then
            // insert the expectation                
        }));
});
akhouri
  • 3,065
  • 3
  • 25
  • 29
  • This is the correct answer, since a test should always know exactly how a spy will be called, and therefore should just use `returnValues` to support multiple calls – Schmuli Jan 16 '17 at 12:23
  • 2
    Just to clarify akhouri's answer: this method only works when the `externalApi.get.and.returnValues('abc','123')` is called within the `it` function. Otherwise if you set a list of values, else where, it shall never work because the order in which tests are ran is not predictable. In fact tests should not depend on the order in which they are executed. – avi.elkharrat Apr 09 '18 at 15:11
0

In my case, I had a component I was testing and, in its constructor, there is a config service with a method called getAppConfigValue that is called twice, each time with different arguments:

constructor(private configSvc: ConfigService) {
  this.configSvc.getAppConfigValue('a_string');
  this.configSvc.getAppConfigValue('another_string');
}

In my spec, I provided the ConfigService in the TestBed like so:

{
  provide: ConfigService,
  useValue: {
    getAppConfigValue: (key: any): any {
      if (key === 'a_string) {
        return 'a_value';
      } else if (key === 'another_string') {
        return 'another_value';
      }
    }
  } as ConfigService
}

So, as long as the signature for getAppConfigValue is the same as specified in the actual ConfigService, what the function does internally can be modified.