5

I'm unit testing an Angular controller that uses a Rails Resource factory to handle GETing and POSTing model data from and to a Rails app. POSTing is done via a method on the model, e.g. (with a model $scope.resource):

$scope.resource.update().then(successHandler, failureHandler);

I have a spy on this method to stub out the Ajax calls so I can unit test the controller:

resUpdateSpy = spyOn($scope.resource, 'update').and.callFake(function() { 
  return {then: function(success, failure){ success(resUpdateResponse); }};
});

In one of my controller methods, I expect the resource to be POSTed with certain data (Stripe data in particular). The data will be overridden after the POST in the same method, so I cannot test the state of the model afterwards. Ideally, I would like to something like:

expect($scope.resource.update).toHaveBeenCalled().whileValueOf($scope.resource.stripeKey).isEqualTo('tok123');

Obviously, this method doesn't exist in vanilla Jasmine. Is there a way in Jasmine (either vanilla or through a third-party project) to test the state of a value when a given spy is called? or is there another way to test this situation – specifically, the state of a model before its data POSTs – that I'm missing?

I'm running Jasmine 2.2.0 with Teaspoon 1.0.2 on an Angular 1.3.14 app.

eirikir
  • 3,802
  • 3
  • 21
  • 39
  • Just to clarify: do you want to test the state of the model even before you've POSTE-ed it (before you called `$scope.resource.update()`) **or** when the response from POST has come and you want to test it before it gets changed in the `then()` handler? – Michael Radionov Jul 29 '15 at 10:24
  • @MichaelRadionov I want to test the state of the model before the POST. Since this is a unit test I'm stubbing the response so there's no point in testing that. – eirikir Jul 29 '15 at 16:19

2 Answers2

3

you can add your own jasime matchers, the one you need would be

jasmine.Matchers.prototype.toBeResolvedWith = function() {
  var done, expectedArgs;
  expectedArgs = jasmine.util.argsToArray(arguments);
  if (!this.actual.done) {
    throw new Error('Expected a promise, but got ' + jasmine.pp(this.actual) + '.');
  }
  done = jasmine.createSpy('done');
  this.actual.done(done);
  this.message = function() {
    if (done.callCount === 0) {
      return ["Expected spy " + done.identity + " to have been resolved with " + jasmine.pp(expectedArgs) + " but it was never resolved.", "Expected spy " + done.identity + " not to have been resolved with " + jasmine.pp(expectedArgs) + " but it was."];
    } else {
      return ["Expected spy " + done.identity + " to have been resolved with " + jasmine.pp(expectedArgs) + " but was resolved with " + jasmine.pp(done.argsForCall), "Expected spy " + done.identity + " not to have been resolved with " + jasmine.pp(expectedArgs) + " but was resolved with " + jasmine.pp(done.argsForCall)];
    }
  };
  return this.env.contains_(done.argsForCall, expectedArgs);
};

more available here https://gist.github.com/gr2m/2191748

updated after comment:

basically jasmine supports custom matchers. some in built matchers are toBe, toEqual, etc. You can add your own custom matcher to check the promise.

var customMatchers = {

toHaveBeenResolved: function(util, customEqualityTesters) {
  return {
    compare: function(actual, expected) {
      var result = {};
      // do you comparison logic here
      result.pass = true/false;
      result.message = 'some message about test result';
      return result;
    }
}

this.actual is a promise and you can resolve it like this

result = {};
promise.then(function(value){
  result.value = value;
  result.status = 'Resolved';
}, function(value){
  result.value = value;
  result.status = 'Rejected';
});

once you have declared you custom matcher use it in you test case in beforeEach callback.

beforeEach(function() {
  jasmine.addMatchers(customMatchers);
}); 
atinder
  • 2,080
  • 13
  • 15
  • Thanks, this looks great, but could you add further explanation? I'm not sure what, exactly, this is testing. Specifically, does `done.argsForCall` represent the model before POST (the latter is what I'm trying to test)? Also, I believe this uses syntax for Jasmine 1, but I'm on Jasmine 2.2.0 (sorry for not including this in original question, I've since updated), and this throws an error that `jasmine.Matchers` is undefined. – eirikir Jul 29 '15 at 17:06
  • @atiender That makes sense on the general level, but I'm also trying to understand in particular how your `toBeResolvedWith` matcher works, so that I can adapt it to my project. Is it invoked like, `expect(myPromise).toBeResolvedWith(someResult)`? Would `this.actual` be the promise, or a representation of the promise, or something else? What is `this.actual.done`? What is `done.identity`? What is being returned? Sorry, I know these are a lot of questions; I'm not usually a JS developer so I'm not as familiar with Jasmine semantics. – eirikir Jul 31 '15 at 15:15
  • updated my answer and for detailed implementation you can take a look at https://github.com/bvaughn/jasmine-promise-matchers http://jsfiddle.net/eitanp461/vjJmY/ – atinder Aug 03 '15 at 04:07
  • I want to test the data *before* POST, but as far as I can tell, this custom matcher only tests the response. Is that correct? – eirikir Aug 04 '15 at 03:56
  • yes it test the response. if you want to check before POST data, create a spy on the method which POST data and `expect(spy).toHaveBeenCalledWith('your data');` – atinder Aug 04 '15 at 04:50
  • The `$scope.resource.update()` call will POST data from `$scope.resource` using the Rails Resource factory. This is stated in my question. How do I test this? – eirikir Aug 04 '15 at 05:00
2

As you already have your spy forward the call to a fake function, have that fake function store the value of interest for you:

var scopeResourceStripeKeyDuringSpyCall = '(spy not called, yet)';
resUpdateSpy = spyOn($scope.resource, 'update').and.callFake(function() {
  scopeResourceStripeKeyDuringSpyCall = $scope.resource.stripeKey;
  return {then: function(success, failure){ success(resUpdateResponse); }};
});

Then just check for the stored-away value in your assertion:

expect(scopeResourceStripeKeyDuringSpyCall).toEqual('tok123');

(Depending on whether spy setup and assertion are in the same scope, the variable for storing the value might have to be less local.)

das-g
  • 9,718
  • 4
  • 38
  • 80