I have a controller that saves a resource. I can't tell how to "access" the part of the code that executes after the promise resolves. What do I need to change about my test or controller in order to get it to work? Here's the code.
Controller:
'use strict';
/**
* #ngdoc function
* #name lunchHubApp.controller:AnnouncementsCtrl
* #description
* # AnnouncementsCtrl
* Controller of the lunchHubApp
*/
angular.module('lunchHubApp')
.controller('AnnouncementsCtrl', ['$scope', 'Announcement', function ($scope, Announcement) {
$scope.announcements = [];
$scope.save = function() {
// This next line is the part I'm finding hard to test.
new Announcement($scope.announcement).create().then(function(announcement) {
$scope.foo = 'bar'
});
};
}]);
Test:
'use strict';
describe('AnnouncementsCtrl', function() {
beforeEach(function() {
module('lunchHubApp', 'ng-token-auth')
});
it('sets scope.announcements to an empty array', inject(function($controller, $rootScope) {
var scope = $rootScope.$new(),
ctrl = $controller('AnnouncementsCtrl', { $scope: scope });
expect(scope.announcements).toEqual([]);
}));
describe('save', function() {
it('works', inject(function($controller, $rootScope, _$httpBackend_) {
var $httpBackend = _$httpBackend_;
var scope = $rootScope.$new(),
ctrl = $controller('AnnouncementsCtrl', { $scope: scope });
expect(scope.announcements.length).toBe(0);
var announcement = {
restaurantName: 'Bangkok Taste',
userId: 1
};
scope.announcement = announcement;
$httpBackend.expect('POST', '/api/announcements').respond(200, announcement);
scope.save();
scope.$digest();
expect(scope.foo).toEqual('bar');
}));
});
});
Update: here's the way I ended up modifying my controller test. The following passes and has been refactored from the original.
'use strict';
describe('AnnouncementsCtrl', function() {
var $httpBackend,
announcement,
scope,
ctrl;
beforeEach(function() {
module('lunchHubApp');
inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
scope = $injector.get('$rootScope').$new();
ctrl = $injector.get('$controller')('AnnouncementsCtrl', { $scope: scope });
announcement = { restaurantName: 'Bangkok Taste' };
scope.announcement = { restaurantName: 'Jason\'s Pizza' };
$httpBackend.expect('GET', '/api/announcements').respond([announcement]);
});
});
it('sets scope.announcements to an empty array', function() {
expect(scope.announcements).toEqual([]);
});
it('grabs a list of announcements', function() {
expect(scope.announcements.length).toBe(0);
$httpBackend.flush();
expect(scope.announcements.length).toBe(1);
});
describe('save', function() {
beforeEach(function() {
$httpBackend.expect('POST', '/api/announcements').respond(200, { restaurantName: 'Foo' });
scope.save();
$httpBackend.flush();
});
it('adds an announcement', function() {
expect(scope.announcements.length).toBe(2);
});
it('clears the restaurant name', function() {
expect(scope.announcement.restaurantName).toEqual('');
});
});
});
I think what you're doing is good. Since the Angular resources are factories using the $http service in a restful way, you should use the expect of the $httpBackend just as you did.
One thing that you miss however is that you need to make sure your promise is resolved. But write async tests can be tricky in some cases. To do so, you have to use the flush() method of $httpBackend to force your test to be synchronous.
After the flush, you can make your expect normally. Also you might have to move your expectPOST before your $rootScope.$new() statement.
You can go with a change like this, I don't think the $digest() is necessary:
$httpBackend.expect('POST', '/api/announcements').respond(200, announcement);
scope.save();
$httpBackend.flush();
expect(scope.foo).toEqual('bar');
The tests you've started writing seem to be testing not just AnnouncementsCtrl, but the Announcements service/factory as well. The signs of this in this case are
You're not mocking the Announcements service/factory / not stubbing any of its methods.
There is no code in the AnnouncementsCtrl regarding making http requests, and yet you're using $httpBackend.expect(... in the tests for them.
The success/failure of the tests that claim to test AnnouncementsCtrl will succeed or fail depending on code in the Announcements service/factory.
This goes against what unit tests are usually used for: testing each component in isolation. Keeping the focus of this answer on testing the success callback passed to the then method of the promise returned by create, my suggestion is to mock the Announcements service/factory, so its create method returns a promise that you can control in the test. This mock would be of the form:
var MockAnnouncement = null;
var deferred = null;
beforeEach(module(function($provide) {
MockAnnouncement = function MockAnnouncement() {
this.create = function() {
return deferred.promise;
};
};
$provide.value('Announcement', MockAnnouncement);
}));
You would then have to make sure that you create deferred object before each test:
beforeEach(inject(function($rootScope, $controller, $q) {
$scope = $rootScope.$new();
deferred = $q.defer(); // Used in MockAnnouncement
ctrl = $controller('AnnouncementsCtrl', {
$scope: $scope
});
}));
This deferred object is then resolved in the test:
it('calls create and on success sets $scope.foo="bar"', function() {
$scope.save();
deferred.resolve();
$scope.$apply();
expect($scope.foo).toBe('bar');
});
A slightly extended version of this, testing a few other behaviours of the controller as well, can be seen at http://plnkr.co/edit/v1bCfmSPmmjBoq3pfDsk
Related
I am trying to be a good developer & write some tests to cover a directive I have. The directive has a service injected in which makes a call to a webApi endpoint.
When I run the test (which at minute expects 1 to equal 2 so I can prove test is actually running!!) I get an error that an unexpected request GET has been made to my real endpoint even though I thought I had mocked/stubbed out the service so test would execute. My test looks something like the below:
I thought that by calling $provide.service with the name of my service and then mocking the method "getUserHoldings" then this would automatically be injected at test time, have I missed a trick here? The path of the endpoint the unexpected request is contained in the actual getUserHoldings method on the concrete service.
Thanks for any help offered as driving me potty!!!
describe('directive: spPlanResults', function () {
var scope;
var directiveBeingTested = '<sp-plan-results></sp-plan-results>';
var element;
beforeEach (module('pages.plans'));
beforeEach (inject(function ($rootScope,
$compile,
currencyFormatService,
_,
moment,
plansModel,
appConfig,
$timeout,
$q,
$provide) {
scope = $rootScope.$new();
$provide.service('plansService', function () {
return {
getUserHoldings: function() {
var deferred = $q.defer();
return deferred.resolve([
{
class: 'Class1',
classId: 2,
award: 'Award1',
awardId : 2
}]);
}
};
});
element = $compile(directiveBeingTested)(scope);
scope.$digest();
});
it ('should be there', inject(function() {
expect(1).equals(2);
}));
});
Referencing - http://www.mikeobrien.net/blog/overriding-dependencies-in-angular-tests/ - it would work if you did your '$provide' configuration in the 'module's context i.e. do something like -
describe('directive: spPlanResults', function () {
var scope;
var directiveBeingTested = '<sp-plan-results></sp-plan-results>';
var element;
beforeEach(module('pages.plans', function($provide) {
$provide.value('plansService', function() {
return {
getUserHoldings: function() {
var deferred = $q.defer();
return deferred.resolve([{
class: 'Class1',
classId: 2,
award: 'Award1',
awardId: 2
}]);
}
};
});
}));
beforeEach(inject(function($rootScope, $compile, currencyFormatService, _, moment, plansModel, appConfig, $timeout, $q) {
scope = $rootScope.$new();
element = $compile(directiveBeingTested)(scope);
scope.$digest();
});
it('should be there', inject(function() {
expect(1).equals(2);
})); });
I'm trying to write a test for an Angular controller that mostly creates a datatable of values from the server. I've tried mocking DTOptionsBuilder and DTColumnBuilder but this doesn't seem to work. I get the error:
'undefined' is not an object (evaluating 'DTOptionsBuilder.fromFnPromise(function(){
return MarketsFactory.getAll();
})
.withDataProp')
Here is the Controller code:
.controller('MarketsCtrl', function($scope, $compile, $state, MarketsFactory,
DTOptionsBuilder, DTColumnBuilder) {
$scope.edit = function(data){
$state.go('admin.market', {id:data});
};
//DATATABLES CONFIGURATIONS
$scope.dtInstance = {};
$scope.dtOptions = DTOptionsBuilder.fromFnPromise(function(){
return MarketsFactory.getAll();
})
.withDataProp('data.data')
.withOption('createdRow', function(row, data, dataIndex) {
$compile(angular.element(row).contents())($scope);
})
.withTableTools('http://cdn.datatables.net/tabletools/2.2.2/swf/copy_csv_xls_pdf.swf')
.withTableToolsButtons([
'copy',
'print', {
'sExtends': 'collection',
'sButtonText': 'Save',
'aButtons': ['csv', 'xls', 'pdf']
}
])
.withBootstrap()
.withBootstrapOptions({
TableTools: {
classes: {
container: 'btn-group right',
buttons: {
normal: 'btn btn-outline btn-default btn-sm'
}
}
}
});
$scope.dtColumns = [
DTColumnBuilder.newColumn('shortName').withTitle('Short Name').withClass('dt-left'),
DTColumnBuilder.newColumn('name').withTitle('Name').withClass('dt-left'),
DTColumnBuilder.newColumn('timezone').withTitle('Time Zone').withClass('dt-left'),
DTColumnBuilder.newColumn('id').renderWith(function(data, type, full) {
return '<a ng-click="edit(\'' + data + '\')">Edit</a>';
})];
})
And the test file:
describe('Controller: MarketsCtrl', function () {
var scope, $state, DTOptionsBuilder, DTColumnBuilder;
beforeEach(function(){
var mockState = {};
var mockDTOptionsBuilder = {};
var mockDTColumnBuilder = {};
module('app', function($provide) {
$provide.value('$state', mockState);
$provide.value('DTOptionsBuilder', mockDTOptionsBuilder);
$provide.value('DTColumnBuilder', mockDTColumnBuilder);
});
inject(function() {
mockState.go = function(target) {
return target;
};
mockDTOptionsBuilder.fromFnPromise = jasmine.createSpy('DTOptionsBuilder.fromFnPromise');
mockDTOptionsBuilder.withDataProp = jasmine.createSpy('DTOptionsBuilder.withDataProp');
mockDTColumnBuilder.newColumn = jasmine.createSpy('DTColumnBuilder.newColumn');
});
});
beforeEach(inject(function ($controller, $rootScope, _$state_, _DTColumnBuilder_, _DTOptionsBuilder_) {
scope = $rootScope.$new();
$state = _$state_;
DTOptionsBuilder = _DTOptionsBuilder_;
DTColumnBuilder = _DTColumnBuilder_;
$controller('MarketsCtrl', {
$scope: scope,
$state: $state,
DTOptionsBuilder: DTOptionsBuilder,
DTColumnBuilder: DTColumnBuilder
});
scope.$digest();
}));
it('should provide an edit function', function () {
expect(typeof scope.edit).toBe('function');
});
});
I thought that creating a mock and putting a Spy on it would prevent it from calling the chained functions, but I guess not.
I'm quite new to testing, especially with Angular, so any help in general would be greatly appreciated!
I'm approaching this from a sinonjs/mocha background
I'm having some trouble decrypting your beforeEach block - but it can be simplified to some extent:
var $scope, $state, DTColumnBuilder, DTOptionsBuilder, createController;
beforeEach(function () {
DTColumnBuilder = {};
DTOptionsBuilder = {};
$state = {};
module('app', function ($provide) {
$provide.value('$state', $state);
$provide.value('DTColumnBuilder', DTColumnBuilder);
$provide.value('DTOptionsBuilder', DTOptionsBuilder);
});
inject(function ($controller, $injector) {
$scope = $injector.get('$rootScope').$new();
$state = $injector.get('$state');
DTColumnBuilder = $injector.get('DTColumnBuilder');
DTOptionsBuilder = $injector.get('DTOptionsBuilder');
createController = function () {
return $controller('MarketsCtrl', {
$scope: scope,
$state: $state,
DTOptionsBuilder: DTOptionsBuilder,
DTColumnBuilder: DTColumnBuilder
});
}
});
// Stub out the methods of interest.
DTOptionsBuilder.fromFnPromise = angular.noop;
$state.go = function () { console.log('I tried to go but.... I cant!!');
DTColumnBuilder.bar = function () { return 'bar'; };
});
The nature of a spy is that of letting the original implementation do its thing, but recording all of the calls to said function and an assortment of associated data.
A stub on the other hand is a spy with an extended API where you can completely modify the workings of said function. Return value, expected parameters, etc etc.
Assuming we used the aforementioned beforeEach block, DTOptionsBuilder.fromFnPromise would be a noop at this point. As such it would be safe to spy on it and expect the method to have been called.
it('should have been called', function () {
var spy = spyOn(DTOPtionsBuilder, 'fromFnPromise');
createController();
expect(spy).toHaveBeenCalled();
});
If you wanted to manipulate the return value of said function, I would grab sinonjs and make it a stub.
it('became "foo"', function () {
DTOptionsBuilder.fromFnPromise = sinon.stub().returns('foo');
createController();
expect($scope.dtOptions).toEqual('foo');
});
Now, since you are working with promises it's a wee bit more complicated but the basics of stubbing out a promise based function would be to:
Inject $q into your spec file.
Tell the stub to return $q.when(/** value **/) in the case of a resolved promise.
Tell the stub to return $q.reject(/** err **/) in the case of a rejected promise.
Run $timeout.flush() to flush all deferred tasks.
Trigger the done callback to notify Jasmine that you are done waiting for async tasks (may not be needed). This depends on the test framework/runner.
It could look something like so:
it('resolves with "foo"', function (done) {
DTOptionsBuilder.fromFnPromise = sinon.stub().returns($q.when('foo'));
expect($scope.options).to.eventually.become('foo').and.notify(done); // this is taken from the chai-as-promised library, I'm not sure what the Jasmine equivalent would be (if there is one).
createController();
$timeout.flush();
});
Now, a lot of this is just guesswork at this point. It's quite hard to set up a fully working test suite without having the source code running right beside me for cross reference, but I hope this will at least give you some ideas of how to get going with your spies and stubs.
I have a controller that takes a dependency on a service, and as part of it's initialisation calls a function on the service. Here's a contrived example:
describe('tests', function() {
var _scope, service, serviceValue = 'value';
beforeEach(module('app'));
beforeEach(inject(['$rootScope','$controller', function($rootScope, $controller) {
_scope = $rootScope.$new();
service = {
get: function(key) {
return serviceValue;
}
};
$controller('myController', {
'$scope': _scope,
'service': service
});
}]));
describe('initialisation', function() {
describe('key exists', function() {
it('should find the key', function() {
expect(_scope.message).toBe('found the key');
});
});
describe('key does not exist', function() {
beforeEach(function() {
serviceValue = undefined;
});
it('should not find the key', function() {
expect(_scope.message).toBe('did not find the key');
});
});
});
});
angular.module('app').controller('myController', ['$scope','service',
function($scope, service) {
if(service.get('key') === 'value') {
$scope.message = 'found the key';
} else {
$scope.message = 'did not find the key';
}
});
The tests for when the key does not exist fail because the controller initialisation has run in the first beforeEach, before the next beforeEach runs to change the service return value.
I can get around this by recreating the whole controller in the beforeEach of the 'key does not exist' tests, but this seems wrong to me, as it initialises the controller twice for the test. Is there a way to get the controller initialisation to run for every test, but after all other beforeEach functions have run.
Is this the right way to be initialising controllers? Am I missing some feature of jasmine?
Creating the controller for each test is the recommended way, especially when you have initialization logic.
I would however use Jasmine's spyOn to set up what the service returns and tracking calls to it, instead of modifying internal values of a mocked or real service.
Inject the real service and save it in a variable, and define a function that creates the controller:
describe('tests', function() {
var $scope, createController, service;
beforeEach(function() {
module('app');
inject(function($rootScope, $controller, _service_) {
$scope = $rootScope.$new();
service = _service_;
createController = function() {
$controller('myController', {
'$scope': $scope,
'service': service
});
};
});
});
For each test use spyOn to intercept calls to the service and decide what it should return, then create the controller:
describe('initialisation', function() {
it('should find the key', function() {
spyOn(service, 'get').and.returnValue('value');
createController();
expect($scope.message).toBe('found the key');
});
it('should not find the key', function() {
spyOn(service, 'get').and.returnValue(undefined);
createController();
expect($scope.message).toBe('did not find the key');
});
});
Demo: http://plnkr.co/edit/BMniTis1RbOR0h5O4kZi?p=preview
As spyOn sets up tracking you can now for example also make sure the service only gets called once on controller initilization:
spyOn(service, 'get').and.returnValue('value');
expect(service.get.calls.count()).toEqual(0);
createController();
expect(service.get.calls.count()).toEqual(1);
Note: The examples above use Jasmine 2.0. Syntaxes will have to be slightly modified for older versions.
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!
My controller is:
angularMoonApp.controller('SourceController', ['$scope', '$rootScope', '$routeParams', 'fileService', function ($scope, $rootScope, $routeParams, fileService) {
$scope.init = function() {
$rootScope.currentItem = 'source';
fileService.getContents($routeParams.path).then(function(response) {
$scope.contents = response.data;
$scope.fileContents = null;
if(_.isArray($scope.contents)) {
// We have a listing of files
$scope.breadcrumbPath = response.data[0].path.split('/');
} else {
// We have one file
$scope.breadcrumbPath = response.data.path.split('/');
$scope.breadcrumbPath.push('');
$scope.fileContents = atob(response.data.content);
fileService.getCommits(response.data.path).then(function(response) {
$scope.commits = response.data;
});
}
});
}
$scope.init();
}]);
My test is pretty simple:
(function() {
describe('SourceController', function() {
var $scope, $rootScope, $httpBackend, createController;
beforeEach(module('angularMoon'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
var $controller = $injector.get('$controller');
createController = function() {
return $controller('SourceController', {
'$scope': $scope
});
};
}));
it("should set the current menu item to 'source'", function() {
createController();
$scope.init();
expect($rootScope.currentItem).toBe('source');
});
it("should get the contents of the root folder", function() {
createController();
$scope.init();
// NOT SURE WHAT TO DO HERE!
});
});
})();
I want to test that the fileService had it's getContents function called and mock a response so that I can test the two scenarios (if is array and if isn't`)
I would recommend using Jasmine spies for this.
Here is an example that might help. I usually put the spyOn call in the beforeEach.
var mockedResponse = {};
spyOn(fileService, "getContents").andReturn(mockedResponse);
In the 'it' part:
expect(fileService.getContents).toHaveBeenCalled();
To get the response, just call the method in your controller that calls the fileService method. You may need to manually run a digest cycle too. Snippet from one of my tests:
var testOrgs = [];
beforeEach(inject(function(coresvc) {
deferred.resolve(testOrgs);
spyOn(coresvc, 'getOrganizations').andReturn(deferred.promise);
scope.getAllOrganizations();
scope.$digest();
}));
it("getOrganizations() test the spy call", inject(function(coresvc) {
expect(coresvc.getOrganizations).toHaveBeenCalled();
}));
it("$scope.organizations should be populated", function() {
expect(scope.allOrganizations).toEqual(testOrgs);
expect(scope.allOrganizations.length).toEqual(0);
});
deferred in this case is a promise created with $q.defer();
You can create a spy and verify only that fileService.getContents is called, or either verify extra calls (like promise resolution) by making the spy call through. Probably you should also interact with httpBackend since you may need to flush the http service (even though you use the mock service).
(function() {
describe('SourceController', function() {
var $scope, $rootScope, $httpBackend, createController, fileService;
beforeEach(module('angularMoon'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
// See here
fileService = $injector.get('fileService');
spyOn(fileService, 'getContents').andCallThrough();
var $controller = $injector.get('$controller');
createController = function() {
return $controller('SourceController', {
'$scope': $scope
'fileService': fileService
});
};
}));
it("should get the contents of the root folder", function() {
createController();
$scope.init();
expect(fileService.getContents).toHaveBeenCalled();
});
});
})();
You can also add expectations to what happens inside the callback but you should issue a httpBackend.flush() before.