Unit test the call count of method in then part of a promise in Jasmine and AngularJS - angularjs

In code below, userService.addPreference is mocked, and so is $state.go, but still the call count of $state.go is always zero. Is there something I may have missed in the setup of userService.addPreference mocked method?
Code that is being unit tested
userService.addPreference(preference).then(function (dashboard) {
$state.go('authenticated.dashboard.grid', {id: dashboard.id});
});
Unit Test Mocked methods and the Unit Test
sinon.stub(userService, 'addPreference', function (preference) {
var defer = $q.defer();
defer.resolve(preference);
return defer.promise;
});
sinon.stub($state, 'go', function () { });
it('dashboard.confirm should call $state.go', function () {
vm.confirm();//this is the function containing code being unit tested
expect($state.go.callCount).to.equal(1);//this is always ZERO and so failing
});

The service call
userService.addPreference(preference).then(function (dashboard) {
$state.go('authenticated.dashboard.grid', {id: dashboard.id});
});
involves a async callback, which will not fire unless we explicitly tell it to. To force the callback to evaluate we need to run a digest cycle using $scope.$apply, so change your test code to:
it('dashboard.confirm should call $state.go', function () {
vm.confirm();//this is the function containing code being unit tested
$scope.$apply();
expect($state.go.callCount).to.equal(1);//this is always ZERO and so failing
});
Remember is a sequential flow callback are never fired.

Related

How to test a method that uses async/await?

I've seen a lot of articles about how use async/await in your unit tests, but my need is the opposite.
How do you write a test for a method that uses async/await?
My spec is not able to reach any code after the 'await' line. Specifically, the spec fails in two ways.
1) HelloWorld.otherCall returns undefined instead of the return value I specify
2) HelloWorld.processResp never gets called
class HelloWorld {
async doSomething(reqObj) {
try {
const val = await this.otherCall(reqObj);
console.warn(val); // undefined
return this.processResp(val);
}
}
}
describe('HelloWorld test', function () {
let sut = new HelloWorld(); //gross simplification for demo purposes
describe('doSomething()', function () {
beforeEach(function mockInputs() {
this.resp = 'plz help - S.O.S.';
});
beforeEach(function createSpy() {
spyOn(sut, 'otherCall').and.returnValue( $q.resolve(this.resp) );
spyOn(sut, 'processResp');
});
it('should call otherCall() with proper arguments', function () {
//this test passes
});
it('should call processResp() with proper arguments', function () {
sut.doSomething({});
$rootScope.$apply(); //you need this to execute a promise chain..
expect(sut.processResp).toHaveBeenCalledWith(this.resp);
//Expected spy processResp to have been called with [ 'plz help SOS' ] but it was never called.
});
});
});
Running angular 1.5 and jasmine-core 2.6.
The .then of a promise is overloaded to handle either promises or values, and await is syntactic sugar for calling then.
So there is no reason your spy would be required to return a promise, or even a value. Returning at all, even if undefined, should trigger the await to fire, and kick off the rest of your async function.
I believe your problem is that you are not waiting for the doSomething promise to resolve before trying to test what it did. Something like this should get you more in the ballpark.
it('should call processResp() with proper arguments', async function () {
await sut.doSomething({});
// ...
});
Jasmine has Asynchronous Support. You can probably find a solution that way.
Personally, I think you should not test such methods at all.
Testing state means we're verifying that the code under test returns the right results.
Testing interactions means we're verifying that the code under test calls certain methods properly.
At most cases, testing state is better.
At your example,
async doSomething(reqObj) {
try {
const val = await this.otherCall(reqObj);
return this.processResp(val);
}
}
As long as otherCall & processResp are well covered by unit tests your good.
Do something should be covered by e2e tests.
you can read more about it at http://spectory.com/blog/Test%20Doubles%20For%20Dummies

Delay testing of a value until a promise has resolved

I'm attempting to test that a value is changed to true after a promise is resolved inside $onInit. I'm following, as best I can, the example in this Stack Overflow question/answer. Here is my code:
class TestCtrl {
constructor(SearchService) {
this.testValue = false;
this.SearchService = SearchService;
}
$onInit() {
this.SearchService.getResults()
.then(function () {
this.testValue = true;
});
}
}
TestCtrl.$inject = ['SearchService'];
And here's the test I'm attempting to run (using mocha, chai, sinon):
it('should work', function() {
ctrl = $componentController('test', {
SearchService: SearchService
}, {});
sinon.stub(SearchService, 'getResults').resolves({response:{data: 'data'}});
ctrl.$onInit();
$rootScope.$apply();
ctrl.testValue.should.equal(true);
});
Should I be testing ctrl.testValue inside a then? Also, is using this example a bad idea because that example doesn't use a component with an $onInit lifecycle hook?
From what I've read, no, "don't use expect inside then in tests." But I'm not so sure based on what I've read elsewhere.
I wouldn't be surprised if I'm missing something obvious in how to test promises (maybe a stub wasn't the way to go?) and/or how to test what happens in the $onInit lifecycle hook.
If the question needs more details, please ask and I'll do my best to add them.
Edit: Checkout you $onInit method:
$onInit() {
this.SearchService.getResults()
.then(function () {
// `this` in anonymous function is reffering to window not the controller instance
this.testValue = true;
});
}
$onInit() {
var self = this;
self.SearchService.getResults()
.then(function () {
self.testValue = true;
});
}
Your example is correct
This is the way to test async code in angularjs - it is tested like synchronous code. Stubs' returning promises are resolved when you execute $rootScope.$apply().
Why it doesn't work
The promise returned from stub.resolves() is not an angular promise. It cannot be triggered to resolve using $rootScope, because it's not a part of angular's world. It's promise resolution queue is tied to something else and hence the need to test like you usually test async code.
Angular doesn't depend on JavaScript's native Promise implementation - it uses a light implementation of Q's promise library that is wrapped in a service called $q
The answer you have quoted uses the same service to create and return a promise from a stub
In order for your code to work - to test like you test synchronous code - you should return a $q promise (By wrapping a value in $q.when(value)) calling $rootScope.$apply() will execute the code in the then block, then proceed with the code below $rootScope.$apply() line.
Here is an example:
it('Sinon should work with angular promises', function () {
var resolved = 'resolved';
var promise = $q.when(resolved);
// Our async function
var stub = sinon.stub().returns(promise);
// Callback to be executed after the promise resolves
var handler = sinon.stub();
stub().then(handler); // async code
// The handler will be called only after $rootScope.$apply()
handler.callCount.should.equal(0);
// triggers digest which will resolve `ready` promises
// like those created with $q.when(), $q.resolve() or those created
// using the $q.defer() and deferred.resolve() was called
// this will execute the code inside the appropriate callback for
// `then/catch/finally` for all promises and then continue
// with the code bellow -> this is why the test is considered `synchronous`
$rootScope.$apply();
// Verify the handler was called and with the expected value
handler.callCount.should.equal(1);
handler.should.have.been.calledWith(resolved);
})
Here it is in action test promise synchronously in angular
For starters, you should read up on how Mocha expects you to test async code.
To start out with the quick bits:
You are on the right path - there are just some bits missing.
Yes you should do your test inside a then.
The example you linked to is fine. Just understand it.
There is absolutely no reason to avoid asserting a test inside a then. In fact, it is usually the only way to assert the resolved value of a promise.
The main problem with your test code is it tries to assert the result before it is available (as promises resolve in a later tick, they are asynchronous).
The main problem with the code you are trying to test is that there is no way of knowing when the init function has resolved.
We can deal with #2 by waiting for the stubbed SearchService.getResults to resolve (as we control the stub in the test), but that assumes too much knowledge of the implementation of onInit, so that is a bad hack.
Instead, we fix the code in TestCtrl, simply by returning the promise in onInit:
//main code / TestCtrl
$onInit() {
return this.SearchService.getResults()
.then(function () {
this.testValue = true;
});
}
Now we can simply wait for any call to onInit to resolve before we test what has happened during its execution!
To fix your test we first add a parameter to the wrapping test function. Mocha will see this and pass in a function that you can call when your test finishes.
it('should work', function(done) {
That makes it an async test. Now lets fix the test part:
ctrl.$onInit().then( () => {
ctrl.testValue.should.equal(true);
done(); // signals to mocha that the test is finished ok
}).catch(done); // pass any errors to the callback
You might find also find this answer enlightening (upvote if it helps you out). After reading it you might also understand why Mocha also supports dropping the done callback by returning a promise from the test instead. Makes for shorter tests:
return ctrl.$onInit().then( () => {
ctrl.testValue.should.equal(true);
});
sinon.stub(SearchService, 'getResults').resolves({response:{data: 'data'}}); is not returning a promise. Use $q.
I would suggest doing this:
ctrl = $componentController('test', {
SearchService: SearchService
}, {});
let deferred =$q.defer();
deferred.resolve({response:{data: 'data'}});
sinon.stub(SearchService, 'getResults').resolves(deferred.promise);
ctrl.$onInit();
$rootScope.$apply();
ctrl.testValue.should.equal(true);
You don't need to test ctrl.testValue inside a then. And generally, I would recommend not assert inside .then() in your specs. The specs will not fail if the promise never gets resolved. That can give you a false sense of security when in reality, your tests are not doing anything. But that's just my opinion.
Your test will pass once the stub returns a promise. Ideally, I would recommend using $httpBackend if the service is making an http call.
Cheers.

Adding test cases for a basic controller for ng-click event using Jasmine

I am trying to lay my hands on karma-jasmine and I am working on some basic examples to start with.
I have created one small plnkr which does 2 things only:
Add the numbers
display some hardcoded values on a button click
Here is my MainController.spec.js:
describe('Main Controller', function () {
var $controller, MainController, UserFactory;
var data = {
"name": "Shashank",
"rank": "1"
}
beforeEach(angular.mock.module('app'));
beforeEach(inject(function (_$controller_, _UserFactory_) {
$controller = _$controller_;
UserFactory = _UserFactory_;
MainController = $controller('MainController', {UserFactory: UserFactory});
spyOn(MainController, 'add').and.callThrough();
spyOn(MainController, 'getHardData').and.callThrough();
}))
it('should be defined', function () {
expect(MainController).toBeDefined();
})
it('should have number = 3', function () {
expect(MainController.number).toEqual(5)
})
it('add() should assign value properly', function () {
// expect(MainController.add).toHaveBeenCalled();
})
it('getHardData should assign the exact data', function () {
// expect(MainController.getHardData).toHaveBeenCalled();
// expect(MainController.data).toEqual(data);
})
})
For the mentioned controller in the attached plnkr, I think I should only test 2 things:
Test that the addition is working correctly.
Test that the data after button is correct
Questions:
Uncommenting any of the above lines thorw error:
Chrome 55.0.2883 (Windows 7 0.0.0) Main Controller add() should assign value properly FAILED
Expected spy add to have been called.
at Object. (controller/MainController.spec.js:28:36)
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0.016 secs / 0.012 secs)
Also, in $controller('MainController', {UserFactory: UserFactory}); , I understand that we are assigning the values of factory but changing it to $controller('MainController', {}); is not throwing any error. WHY?
Please help me understand my silly mistakes.
Any good links to understand best practices/examples of karma & jasmine would be of great help
Looking at the controller, it exposes four properties - two which are functions (five properties if you count sumVal that is created in the add function).
The properties data and number can simply be asserted that they are initialized correctly after controller creation, like you have done with one of them.
When it comes to the add function, the most obvious test is that it should add the two arguments and assign the result correctly. As the function takes two parameters, there are plenty of other tests you can add. Calling the function without arguments, calling the function with one number and one string etc. If you want to test this usually depends on the use case and your testing strategy.
The getHardData function doesn't take any arguments so what you want to test is that it uses the UserFactory and that it assigns the result correctly.
The reason the tests fail if you uncomment the code is simply because the functions are never called. There is nothing in your controller that calls the add or getHardData methods upon controller initialization and there is nothing in your test that calls them.
For the test to work you would have to do:
it('add() should assign value properly', function() {
MainController.add();
expect(MainController.add).toHaveBeenCalled();
});
This is a useless test however, since you are not testing any of your own functionality.
Instead, you should for example do this:
it('add() should assign value properly', function() {
expect(MainController.sumVal).toBeUndefined();
MainController.add(5, 10);
expect(MainController.sumVal).toBe(15);
});
You don't need to spy on the controller methods that you are testing, since you will be calling them manually.
You want to use spies when you have a controller method that calls another dependency, like getHardData.
You don't want the real implementation of UserFactory to be used in the test, so you can use a spy to interrupt the call and control what it returns.
For example:
it('getHardData should assign the exact data', function() {
// Set up the spy and the return value
spyOn(UserFactory, 'getData').and.returnValue(data);
// Call the method that you are testing
MainController.getHardData();
// Assert that the dependency was called
expect(UserFactory.getData).toHaveBeenCalled();
// Assert that the returned data was assigned correctly
expect(MainController.data).toEqual(data);
});
An alternative is to create a mock implementation and use that in your controller:
var MainController,
data = {
name: "Shashank",
rank: "1"
},
UserFactory = {
getData: function() {
return data;
}
};
beforeEach(function() {
angular.mock.module('app');
inject(function($controller) {
MainController = $controller('MainController', {
UserFactory: UserFactory
});
});
});
it('getHardData should assign the exact data', function() {
// Set up the spy
spyOn(UserFactory, 'getData').and.callThrough();
// Call the method that you are testing
MainController.getHardData();
// Assert that the dependency was called
expect(UserFactory.getData).toHaveBeenCalled();
// Assert that the returned data was assigned correctly
expect(MainController.data).toEqual(data);
});
Note that the spy now uses and.callThrough() instead of and.returnValue(data). If you don't use this the call will be interrupted by the spy and your mock implementation will not be used.
The reason that $controller('MainController', {}); isn't throwing any error is because when passing an empty object as the second argument (or no argument at all - $controller('MainController')) the real dependencies will be injected automatically, which is the same as you have now.

Jasmine spec has no expectation

I have the following code:
(beforeEach)
spyOn(HttpService, 'post').and.callFake(function (url, paging, targetSpinner) {
return $q.when(_fakeServerResponse);
});
The test case:
it('should compare size', function () {
service.get({},'','').then(function (serviceResponse) {
expect(serviceResponse.x).toEqual(_fakeServerResponse.x);
and the get method:
return httpService.post(apiUrls).then(postComplete)
My problem is, as mentioned in the title: some why jasmine says that there are not expectations.
The service itself when not running a test is used:
myService.get(data, param1, param2).then(getComplete);
I will also add that when running the spec case, getComplete is never called, which is the source of the problem as I see it (yet I don't know why it doesn't get called).
Thanks
$q is asynchronous. The returned promise will only be resolved at the next scope digest. Your test should rather look like:
it('should compare size', inject(function($rootScope) {
var actualX;
service.get({},'','').then(function(serviceResponse) {
actualX = serviceResponse.x;
};
$rootScope.$apply(); // that will actually resolve the promise
expect(actualX).toEqual(_fakeServerResponse.x);
}));

How to test an AngularJS controller value that is set within a promise using Sinon

I'm having troubling testing a controller's value that's set within a promise returned by a service. I'm using Sinon to stub the service (Karma to run the tests, Mocha as the framework, Chai for assertions).
I'm less interested in a quick fix than I am in understanding the problem. I've read around quite a bit, and I have some of my notes below the code and the test.
Here's the code.
.controller('NavCtrl', function (NavService) {
var vm = this;
NavService.getNav()
.then(function(response){
vm.nav = response.data;
});
})
.service('NavService', ['$http', function ($http) {
this.getNav = function () {
return $http.get('_routes');
};
}]);
Here's the test:
describe('NavCtrl', function () {
var scope;
var controller;
var NavService;
var $q;
beforeEach(module('nav'));
beforeEach(inject(function($rootScope, $controller, _$q_, _NavService_){
NavService = _NavService_;
scope = $rootScope.$new();
controller = $controller;
}));
it('should have some data', function () {
var stub = sinon.stub(NavService, 'getNav').returns($q.when({
response: {
data: 'data'
}
}));
var vm = controller("NavCtrl", {
$scope: scope,
NavService: NavService
});
scope.$apply();
stub.callCount.should.equal(1);
vm.should.be.defined;
vm.nav.should.be.defined;
});
});
The stub is being called, i.e. that test passes, and vm is defined, but vm.nav never gets data and the test fails. How I'm handling the stubbed promise is, I think, the culprit. Some notes:
Based on reading elsewhere, I'm calling scope.$apply to set the value, but since scope isn't injected into the original controller, I'm not positive that will do the trick. This article points to the angular docs on $q.
Another article recommends using $timeout as what would "actually complete the promise". The article also recommends using "sinon-as-promised," something I'm not doing above. I tried, but didn't see a difference.
This Stack Overflow answer use scope.$root.$digest() because "If your scope object's value comes from the promise result, you will need to call scope.$root.$digest()". But again, same test failure. And again, this might be because I'm not using scope.
As for stubbing the promise, I also tried the sinon sandbox way, but results were the same.
I've tried rewriting the test using $scope, to make sure it's not a problem with the vm style, but the test still fails.
In the end, I could be wrong: the stub and the promise might not be the problem and it's something different and/or obvious that I've missed.
Any help is much appreciated and if I can clarify any of the above, let me know.
Sorry but a quick fix was all that you needed:
var stub = sinon.stub(NavService, 'getNav').returns($q.when({
response: {
data: 'data'
}
}));
Your promise is resolved to object containing response.data not just data
Checkout this plunk created from your code: https://plnkr.co/edit/GL1Xuf?p=preview
The extended answer
I have often fallen to the same trap. So I started to define the result returned from a method separately. Then if the method is async I wrap this result in a promise like $q.when(stubbedResult) this allow me to, easily run expectations on the actual result, because I keep the stubbed result in a variable e.g.
it('Controller should have some data', function () {
var result = {data: 'data'};
var stub = sinon.stub(NavService, 'getNav').returns($q.when(result));
var vm = controller(/* initController */);
scope.$apply();
stub.callCount.should.equal(1);
vm.nav.should.equal(result.data)
})
Also some tests debugging skill will come in handy. The easiest thing is to dump some data on the console just to check what's returned somewhere. Working with an actual debugger is preferable of course.
How to quickly catch mistakes like these:
Put a breakpoint at the $rootScope.apply() line (just before it is executed)
Put a breakpoint in the controller's NavService.getNav().then handler to see whether it is called and what it was called with
Continue with the debugger to execute the $rootScope.$apply() line. Now the debugger should hit the breakpoint set at the previous step - that's it.
I think you should use chai-as-promised
and then assert from promises like
doSomethingAsync().should.eventually.equal("foo");
or else use async await
it('should have some data', async function () {
await scope.$apply();
});
you might need to move then getNav() call in init kinda function and then test against that init function

Resources