So I have a controller like this:
angular.module('someModule').controller('someController',function(productService) {
$scope.products = [];
$scope.init = function() {
aService.fetchAll().then(function(payload) {
$scope.products = filterProducts(payload.data);
});
}
$scope.init();
function filterProducts(products) {
//for each of products filter some specific ones
return filteredProducts;
}
});
I am writing a test that will call the $scope.init() and has to verify that the products were filtered appropriately. I am mocking the $httpBackend so the code looks like this:
describe("someController", function() {
"use strict";
var $controller; //factory
var controller; //controller
var $rootScope;
var $state;
var $stateParams;
var $injector;
var $scope;
var $httpBackend;
var productService;
beforeEach(function(){
angular.mock.module("someModule")
inject(function (_$rootScope_, _$state_, _$injector_, $templateCache, _$controller_, _$stateParams_, _$httpBackend_, _productService_) {
$rootScope = _$rootScope_;
$state = _$state_;
$stateParams = _$stateParams_;
$injector = _$injector_;
$controller = _$controller_;
$httpBackend = _$httpBackend_;
productService = _productService_;
});
controller = $controller("someController", {$scope: $scope, $state: $state});
});
it("init() should filter products correctly",function(){
//Arrange
var expectedFilteredProducts = ["1","2"];
var products = ["0","1","2"];
$httpBackend.whenGET("api/products").respond(products);
//Act
$scope.init();
//Assert
setTimeout(100,function(){
expect($scope.products).toEqual(expectedFilteredProducts);
});
$httpBackend.flush();
});
});
The problem is that without the setTimeout the test doesn't pass. Is there a way to test what I am trying to do without it and without introducing complex $q/promises just for the test? As a side note productService is returning a promise $http. Thanks.
Edit: setTimeout makes test run but no assertions are happening..
Based on the comments, the problem was that the $httpBackend.flush() needs to be executed before the expectations. Effectively it resolves all requests it was trained to resolve (and their respective promises) so that expectations can run on completed promises.
It is also a good idea to use $httpBackend.verifyNoOutstandingExpectation() / $httpBackend.verifyNoOutstandingRequest() after the tests.
Also note that the controller has already called scope.init(), so calling it again in the test is redundant - and may even cause failures depending on the exact case.
Also for the setTimeout running but not asserting anything: Using it in a spec makes this spec asynchronous. You will have to define the spec with the done callback and call it from the asynchronous code, when the test is really finished, as:
it("init() should filter products correctly",function(done) {
...
//Assert
setTimeout(100, function() {
...
done();
});
});
Note again that Angular mocks provide ways to avoid using setTimeout for 99.9% of the time, even the $interval/$timeout services are properly mocked!
Related
I'm new to jasmine testing. How can I test function call in watch function?
Below is my code. I'm confused about usage of spy in jasmine and how can I handle function call inside watcher.
Do I need to pause fetch() inside watch. Please suggest how to improve my testing skills.
var app = angular.module('instantsearch',[]);
app.controller('instantSearchCtrl',function($scope,$http,$sce){
$scope.$sce=$sce;
$scope.$watch('search', function() {
fetch();
});
$scope.search = "How create an array";
var result = {};
function fetch() {
$http.get("https://api.stackexchange.com/2.2/search?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!4*Zo7ZC5C2H6BJxWq&key=DIoPmtUvEkXKjWdZB*d1nw((")
.then(function(response) {
$scope.items = response.data.items;
$scope.answers={};
angular.forEach($scope.items, function(value, key) {
var ques = value.question_id;
$http.get("https://api.stackexchange.com/2.2/questions/"+value.question_id+"/answers?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!9YdnSMKKT&key=DIoPmtUvEkXKjWdZB*d1nw((").then(function(response2) {
$scope.answers[ques]=response2.data.items;
//console.log(JSON.stringify($scope.answers));
});
});
});
}
});
my test case:
describe('instantSearchCtrl', function() {
beforeEach(module('instantsearch'));
var $scope, ctrl;
beforeEach( inject(function($rootScope, $controller) {
// create a scope object for us to use.
$scope = $rootScope.$new();
ctrl = $controller('instantSearchCtrl', {
$scope: $scope
});
}));
/*var $scope = {};
var controller = $controller('instantSearchCtrl', { $scope: $scope });
expect($scope.search).toEqual('How create an array');
//expect($scope.strength).toEqual('strong');*/
it('should update baz when bar is changed', function (){
//$apply the change to trigger the $watch.
$scope.$apply();
//fetch().toHaveBeenCalled();
fetch();
it(" http ", function(){
//scope = $rootScope.$new();
var httpBackend;
httpBackend = $httpBackend;
httpBackend.when("GET", "https://api.stackexchange.com/2.2/search?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!4*Zo7ZC5C2H6BJxWq&key=DIoPmtUvEkXKjWdZB*d1nw((").respond([{}, {}, {}]);
});
});
});
First you should trigger the watch. For that you should change search value and after that manually run: $scope.$digest() or $scope.$apply()
In order to fully test the fetch function you should also mock the response to the second request (or the mock for all the second level requests if you want to test the iteration).
After that you should add some expect statements. For the controller code they should be:
expect($scope.items).toEqual(mockResponseToFirstRequest);
expect($scope.answers).toEqual(mockResponseCombinedForSecondLevelRequests);
As for using the spy in karma-jasmine tests, those limit the amount of the code tested. A plausible use for spy in this case is to replace the httpBackend.when with spyOn($http, 'get').and.callFake(function () {})
Here is the documentation for using spies https://jasmine.github.io/2.0/introduction.html#section-Spies
We're just getting started with unit testing in our Angular app and we are using a karma/mocha/chai framework for unit testing. I carried out some basic unit tests on various services and factories we have defined and it the unit testing works great. However now we want to test some controller and evaluate the scope vars that the controllers are modifying.
Here is an example of one such controller:
angular.module('App').controller('myCtrl', ['$scope', 'APIProxy',
function ($scope, APIProxy) {
$scope.caseCounts = {caseCount1: 0, caseCount2: 0};
$scope.applyCounts = function () {
$scope.caseCounts.caseCount1 = {...some case count logic...}
$scope.caseCounts.caseCount2 = {...some case count logic...}
};
APIProxy.getAll().then(function (data) {
{...do a bunch of stuff...}
$scope.data = data;
$scope.applyCounts();
});
}]
);
Now, when I unit test I would like to start off with just a simple 'does the $scope.caseCounts have values > 0, then I'll build from there. However it is not obvious how to make the controller cause the APIProxy service run and how to handle the eventual return of data. We've tried $scope.getStatus(), and $scope.apply() and a few other things but I feel like we are way off the mark and we are fundamentally missing something about how to go about this.
Currently our controller tester looks like:
describe("myCtrl unit tests",function(){
beforeEach(module('App'));
var ctrl, APIProxy;
beforeEach(inject(function ($rootScope, $controller, _APIProxy_)
{
$scope = $rootScope.$new();
APIProxy = _APIProxy_;
ctrl = $controller('myCtrl', {$scope: $scope, APIProxy: APIProxy});
}));
it('Loads data correctly', function() {
expect(ctrl).to.not.be.undefined;
//??? what else do we do here to fire the getAll function in controller?
});
});
It's usually better to test the service and the controller separately.
To test the service, you can use $httpBackend to mock the XHR requests:
https://docs.angularjs.org/api/ngMock/service/$httpBackend
To test the controller, you can simply provide mocked values instead of the actual service when you initalize the controller
APIProxy = {'mocked':'data'};
ctrl = $controller('myCtrl', {$scope: $scope, APIProxy: APIProxy});
Or more generally, to mock any provider of your module:
module(function($provide) {
$provide.constant('ab','cd');
$provide.value('ef', 'gh');
$provide.service('myService', function() { });
});
Which will override the 'myService' referenced as dependencies in your controller (if there is one). If you need it directly, you can then inject it too:
var myService;
beforeEach(inject(function (_myService_) {
myService = _myService_;
}));
If you need APIProxy to return a promise, you can mock it too with
https://docs.angularjs.org/api/ng/service/$q
and resolve, e.g.:
var deferred = $q.defer();
deferred.resolve({'mocked':'data'});
return deferred.promise;
If you do want to test them together, you can do a spy on the API function you call and have the spy return a resolved promise.
I did this controller
app.controller('controller',['$scope','httpServices',function($scope,httpServices){
$scope.items= undefined;
httpServices.getItems( function(items){
$scope.items= items;
});
}]);
and I wrote this test
describe('controller', function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('controller', {
'$scope': scope
});
}));
it('defined', function () {
expect(scope.items).toBeUndefined();
})
});
How I can test the scope.items after to have called the service?
I assume that your service httpServices is making some http requests. Therefore you should use the mock-backend service in order to test your controller.
Something like this, pay attention to the comments that I've made inside the code:
describe('Your specs', function() {
var $scope,
$controller,
$httpBackend;
// Load the services's module
beforeEach(module('yourApp'));
beforeEach(inject(function(_$controller_, $rootScope, _$httpBackend_) {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$controller = _$controller_;
//THIS LINE IS VERY IMPORTANT, HERE YOU HAVE TO MOCK THE RESPONSE FROM THE BACKEND
$httpBackend.when('GET', 'http://WHATEVER.COM/API/SOMETHING/').respond({});
var createController = function(){
$controller('controller', {$scope: $scope});
}
}));
describe('Your controller', function() {
it('items should be undefined', function() {
createController();
expect(scope.items).toBeUndefined();
});
it('items should exist after getting the response from the server', function () {
//THIS LINE IS ALSO VERY IMPORTANT, IT EMULATES THE RESPONSE FROM THE SERVER
$httpBackend.flush();
expect(scope.items).toBeDefined();
});
});
});
The question title states this is to test a service, but the code of the question looks like an attempt is being made to test the controller. This answer describes how to test the controller.
If you're testing the controller that calls httpServices.getItems, then you need to mock it/stub getItems in order to
Control it on the test
Not assume any behaviour of the real httpServices.getItems. After all, you're testing the controller, and not the service.
A way to do this is in a beforeEach block (called before the controller is created) provide a fake implementation of getItems that just saves the callback passed to it.
var callback;
beforeEach(inject(function(httpServices) {
callback = null;
spyOn(httpServices, 'getItems').and.callFake(function(_callback_) {
callback = _callback_;
});
});
In the test you can then call this callback, passing in some fake data, and test that this has been set properly on the scope.
it('saves the items passed to the callback on the scope', function () {
var testItems = {};
callback(testItems);
expect($scope.items).toBe(testItems);
});
This can be seen working at http://plnkr.co/edit/Z7N6pZjCS9ojs9PZFD04?p=preview
If you do want to test httpServices.getItems itself, then separate tests are the place for that. Assuming getItems calls $http, then you are most likely to need to use $httpBackend to handle mock responses. Most likely, these tests would not instantiate any controller, and I suspect not need to do anything on any scope.
I put the code in a fiddle so it can be easily updated and 'worked with' if needed.
describe('PlayersListCtrl', function() { // Jasmine Test Suite
beforeEach(module('wc2014App'));
var ctrl, scope, $httpBackend;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
ctrl = $controller('PlayersListCtrl', {
$scope: scope
});
}));
it('should have an empty player array', function() {
expect(scope.players.length).toBe(0);
});
describe('PlayersListCtrl', function() {
var $httpBackend, $rootScope, createController;
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$httpBackend.when('GET', '../app/stubs/players.json').respond(
{userId: 'userX'},
{'A-Token': 'xxx'});
$rootScope = $injector.get('$rootScope');
var $controller = $injector.get('$controller');
createController = function() {
return $controller('PlayersListCtrl', {'$scope' : $rootScope });
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should fetch authentication token', function() {
$httpBackend.expectGET('../app/stubs/players.json');
var controller = createController();
$httpBackend.flush();
});
});
});
The rest, cause its quite verbose, is in the fiddle: http://jsfiddle.net/tLte2/
Basically the first test passes, not a hard one, but the second one depends on a JSON stub and gives errors like:
PhantomJS 1.9.7 (Mac OS X) PlayersListCtrl PlayersListCtrl should fetch authentication token FAILED
Error: No pending request to flush !
Cant seem to get a grip on how this $httpBackend stiff works. Is must be possible to just fire it and set the result in the scope of the controller?
--edit
Basically got everything wired up perfectly and can do some simple tests that run just fine, however getting JSON stub data in there seems to be a pain. Workaround can be just defining the array described in the the JSON on the controller scope like: controller.players = ['one','two','three',..... etc ......]
But that doesnt feel right. That $httpBackend stuff shouldn't be that hard to fix right?
I'm trying to write a karma/jasmine test and I would like some explanations about how mocks are working on a service which is returning a promise. I explain my situation :
I have a controller in which I do the following call :
mapService.getMapByUuid(mapUUID, isEditor).then(function(datas){
fillMapDatas(datas);
});
function fillMapDatas(datas){
if($scope.elements === undefined){
$scope.elements = [];
}
//Here while debugging my unit test, 'datas' contain the promise javascript object instead //of my real reponse.
debugger;
var allOfThem = _.union($scope.elements, datas.elements);
...
Here is how my service is :
(function () {
'use strict';
var serviceId = 'mapService';
angular.module('onmap.map-module.services').factory(serviceId, [
'$resource',
'appContext',
'restHello',
'restMap',
serviceFunc]);
function serviceFunc($resource, appContext, restHello, restMap) {
var Maps = $resource(appContext+restMap, {uuid: '#uuid', editor: '#editor'});
return{
getMapByUuid: function (uuid, modeEditor) {
var maps = Maps.get({'uuid' : uuid, 'editor': modeEditor});
return maps.$promise;
}
};
}
})();
And finally, here is my unit test :
describe('Map controller', function() {
var $scope, $rootScope, $httpBackend, $timeout, createController, MapService, $resource;
beforeEach(module('onmapApp'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
var $controller = $injector.get('$controller');
createController = function() {
return $controller('maps.ctrl', {
'$scope': $scope
});
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
var response = {"elements":[1,2,3]};
it('should allow user to get a map', function() {
var controller = createController();
$httpBackend.expect('GET', '/onmap/rest/map/MY-UUID?editor=true')
.respond({
"success": response
});
// hope to call /onmap/rest/map/MY-UUID?editor=true url and hope to have response as the fillMapDatas parameter
$scope.getMapByUUID('MY-UUID', true);
$httpBackend.flush();
});
});
What I really want to do is to have my response object ( {"elements:...}) as the datas parameter of the fillMapDatas function. I don't understand how to mock all the service things (service, promise, then)
So you want to test, if your service responses as expected? Then, this is something you would rather test on the service. Unit test promise based methods could look like this:
var mapService, $httpBackend, $q, $rootScope;
beforeEach(inject(function (_mapService_, _$httpBackend_, _$q_, _$rootScope_) {
mapService = mapService;
$httpBackend = _$httpBackend_;
$q = _$q_;
$rootScope = _$rootScope_;
// expect the actual request
$httpBackend.expect('GET', '/onmap/rest/map/uuid?editor=true');
// react on that request
$httpBackend.whenGET('/onmap/rest/map/uuid?editor=true').respond({
success: {
elements: [1, 2, 3]
}
});
}));
As you can see, you don't need to use $injector, since you can inject your needed services directly. If you wanna use the correct service names throughout your tests, you can inject them with prefixed and suffixed "_", inject() is smart enough to recognise which service you mean. We also setup the $httpBackend mock for each it() spec. And we set up $q and $rootScope for later processing.
Here's how you could test that your service method returns a promise:
it('should return a promise', function () {
expect(mapService.getMapUuid('uuid', true).then).toBeDefined();
});
Since a promise always has a .then() method, we can check for this property to see if it's a promise or not (of course, other objects could have this method too).
Next you can test of the promise you get resolves with the proper value. You can do that setting up a deferred that you explicitly resolve.
it('should resolve with [something]', function () {
var data;
// set up a deferred
var deferred = $q.defer();
// get promise reference
var promise = deferred.promise;
// set up promise resolve callback
promise.then(function (response) {
data = response.success;
});
mapService.getMapUuid('uuid', true).then(function(response) {
// resolve our deferred with the response when it returns
deferred.resolve(response);
});
// force `$digest` to resolve/reject deferreds
$rootScope.$digest();
// make your actual test
expect(data).toEqual([something]);
});
Hope this helps!