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.
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 just start with tests in AngularJS. Please help me to fix it.
My cript
angular.module('test', [])
.controller('ctrl', ['$scope', 'svc', function ($scope, svc) {
$scope.data = [];
svc.query()
.then(function (data) {
$scope.data = data;
});
}]);
and test spec
describe('ctrl', function () {
var ctrl, scope, svc, def, data = [{name: 'test'}];
beforeEach(module('test'));
beforeEach(inject(function($controller, $rootScope, $q) {
svc = {
query: function () {
def = $q.defer();
return def.promise;
}
};
var a=jasmine.createSpy(svc, 'query');
scope = $rootScope.$new();
controller = $controller('ctrl', {
$scope: scope,
svc: svc
});
}));
it('should assign data to scope', function () {
def.resolve(data);
scope.$digest();
expect(svc.query).toHaveBeenCalled();
expect(scope.data).toBe(data);
});
});
It fail:Error: Expected a spy, but got Function. in http://cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js (line 2125). Can you help me
You are getting that error because its failing on expect method. expect method is expecting a spy to be passed in but its not. To fix this problem do:
spyOn(svc, 'query').andCallThrough();
You're creating a spy using createSpy(), which returns a function you can spy on, but you nere use it. You're making your life more complex than it should be. Just let angular inject the real service, and spy on its query() function. Also, use $q.when() to create a resolved promise.
describe('ctrl', function () {
var scope, svc;
var data = [{name: 'test'}];
beforeEach(module('test'));
beforeEach(inject(function($controller, $rootScope, $q, _svc_) {
svc = _svc_;
spyOn(svc, 'query').andReturn($q.when(data));
scope = $rootScope.$new();
$controller('ctrl', {
$scope: scope,
});
}));
it('should assign data to scope', function () {
scope.$digest();
expect(svc.query).toHaveBeenCalled();
expect(scope.data).toBe(data);
});
});
I am following the angular-test-patterns guide, and I get it working with my first controller test. But when I write the next test, I get the error:
TypeError: 'undefined' is not an object (evaluating '$scope.pages.$promise')
The problem then I know is the following line:
$scope.busy = $scope.pages.$promise;
But I don't know how to deal with this, especially since I am very new in test issues with JavaScript. I looking for a correct and viable way of doing this, to point me in the right direction.
The controller:
angular.module('webvisor')
.controller('page-list-controller', function($scope,Page){
$scope.pages = Page.query();
$scope.busy = $scope.pages.$promise;
});
Service:
angular.module('webvisor').
factory('Page', ['$resource', 'apiRoot', function($resource, apiRoot) {
var apiUrl = apiRoot + 'pages/:id/:action/#';
return $resource(apiUrl,
{id: '#id'},
{update: {method: 'PUT'}}
);
}]);
Test:
'use strict';
describe('Controller: page-list-controller', function () {
var ctrl, scope, rootScope, Page;
beforeEach(function () {
module('webvisor');
module(function ($provide) {
$provide.value('Page', new MockPage());
});
inject(function ($controller, _Page_) {
scope = {};
rootScope = {};
Page = _Page_;
ctrl = $controller('page-list-controller', {
$scope: scope,
$rootScope: rootScope
});
});
});
it('should exist', function () {
expect(!!ctrl).toBe(true);
});
describe('when created', function () {
// Add specs
});
});
Mock:
MockPage = function () {
'use strict';
// Methods
this.query = jasmine.createSpy('query'); // I dont know if this is correct
return this;
};
With Mox, your solution would look like this:
describe('Controller: page-list-controller', function () {
var mockedPages = []; // This can be anything
beforeEach(function () {
mox
.module('webvisor')
.mockServices('Page') // Mock the Page service instead of $httpBackend!
.setupResults(function () {
return {
Page: { query: resourceResult(mockedPages) }
};
})
.run();
createScope();
createController('page-list-controller');
});
it('should get the pages', function () {
expect(this.$scope.pages).toEqual(resourceResult(mockedPages));
});
});
As you see, Mox has abstracted away all those boilerplate injections like $rootScope and $controller. Futhermore there is support for testing resources and promises out of the box.
Improvements
I advise you not to put the resource result directly on the scope, but resolve it as a promise:
$scope.busy = true;
Pages.query().$promise
.then(function (pages) {
$scope.pages = pages;
$scope.busy = false;
});
The Mox test is just this:
expect(this.$scope.busy).toBe(true);
this.$scope.$digest(); // Resolve the promise
expect(this.$scope.pages).toBe(mockedPages);
expect(this.$scope.busy).toBe(false);
NB: You also can store the result of createScope() into a $scope var and reuse that everywhere, instead of accessing this.$scope.
After some research and many try and error cases, I answer myself with a possible solution, but I expect to find some more usable and not too repetitive soon. For now, I am satisfied with this, using $httpBackend.
Test:
'use strict';
describe('Controller: page-list-controller', function () {
var ctrl, scope, rootScope, httpBackend, url;
beforeEach(function () {
module('webvisor');
inject(function ($controller, $httpBackend, apiRoot) {
scope = {};
rootScope = {};
httpBackend = $httpBackend;
url = apiRoot + 'pages/#';
ctrl = $controller('page-list-controller', {
$scope: scope,
$rootScope: rootScope
});
});
});
it('should exist', function () {
expect(!!ctrl).toBe(true);
});
describe('when created', function () {
it('should get pages', function () {
var response = [{ 'name': 'Page1' }, { 'name': 'Page2' }];
httpBackend.expectGET(url).respond(200, response);
httpBackend.flush();
expect(scope.pages.length).toBe(2);
});
});
});
I found this solution reading this question. This work very well, and for now, satisfied me. In future, I tried somethig like those:
angular-easy-test
mox
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
I'm using Jasmine to unit test an Angular controller which has a method that runs asynchronously. I was able to successfully inject dependencies into the controller but I had to change up my approach to deal with the async because my test would run before the data was loaded. I'm currently trying to spy on the mock dependency and use andCallThrough() but it's causing the error TypeError: undefined is not a function.
Here's my controller...
myApp.controller('myController', function($scope, users) {
$scope.user = {};
users.current.get().then(function(user) {
$scope.user = user;
});
});
and my test.js...
describe('myController', function () {
var scope, createController, mockUsers, deferred;
beforeEach(module("myApp"));
beforeEach(inject(function ($rootScope, $controller, $q) {
mockUsers = {
current: {
get: function () {
deferred = $q.defer();
return deferred.promise;
}
}
};
spyOn(mockUsers.current, 'get').andCallThrough();
scope = $rootScope.$new();
createController = function () {
return $controller('myController', {
$scope: scope,
users: mockUsers
});
};
}));
it('should work', function () {
var ctrl = createController();
deferred.resolve('me');
scope.$digest();
expect(mockUsers.current.get).toHaveBeenCalled();
expect(scope.user).toBe('me');
});
});
If there is a better approach to this type of testing please let me know, thank you.
Try
spyOn(mockUsers.current, 'get').and.callThrough();
Depends on the version you have used: on newer versions andCallThroungh() is inside the object and.
Here the documentation http://jasmine.github.io/2.0/introduction.html