I have the following function that I would like to spy... but it contains a promise... But I am getting TypeError: 'undefined' is not an object (evaluating 'modalService.showModal({}, modalOptions).then')
Because of course I have just spyOn(modalService,'showModal')
How do I account for the promise too so ??
_modalService = {
close: function (value) { console.log(value) },
dismiss: function (value) { console.log(value) },
showModal: function (value) { console.log(value) }
};
spyOn(_modalService, 'close');
spyOn(_modalService, 'dismiss');
spyOn(_modalService, 'showModal');
Controller function:
user.resetPassword = function () {
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Reset',
headerText: 'Reset Password',
bodyText: 'Are you sure you want to reset the users password?'
};
modalService.showModal({}, modalOptions).then(function (result) {
if (result === 'ok') {
userDataService.resetPassword(user.data).then(function (result) {
$scope.$emit('showSuccessReset');
});
};
});
};
Here is my unit test:
it('should allow the users password to be reset', function () {
var controller = createController();
controller.resetPassword();
$httpBackend.flush();
})
*******************UPDATE
So I change it to this:
//Create a fake instance of the modal instance. TO ensure that the close is called
_modalService = {
close: function (value) { console.log(value) },
dismiss: function (value) { console.log(value) },
showModal: function (value) { console.log(value) }
};
spyOn(_modalService, 'close');
spyOn(_modalService, 'dismiss');
spyOn(_modalService, 'showModal').and.callThrough();
_modalService.showModal = function() {
var deferred = $q.defer();
deferred.resolve('Remote call result');
return deferred.promise;
};
To be honest though I am not sure I could explain this. While I understand all the async stuff... I am not sure how jasmine is using this to make it all work. Can anyone explain the flow???? Also I feel the syntax is wrong... how would you typically write this so it looks better/cleaner...??
When you need to mock a function that returns a promise, you have two options:
Return a mocked promise (an object that resembles a promise);
Return a real promise.
I suggest #2 because it's easier and you don't have to worry about replicating the whole promise API. In other words, it isn't worth mocking a promise itself.
Now about Jasmine: you only need to use spyOn when you already have an object (not a mock) and you want to spy on (no pun intended) one of its methods. In your case, your whole object is fake, so you could use jasmine.createSpyObj instead.
The following example should make all of the above clearer:
SUT
app.controller('MainCtrl', function($scope, modal, service) {
$scope.click = function() {
modal.show().then(function(result) {
if (result === 'ok') {
service.resetPassword();
}
});
};
});
Test
describe('Testing a controller', function() {
var $scope, $q,
ctrl, modalMock, serviceMock;
beforeEach(function() {
module('plunker');
modalMock = jasmine.createSpyObj('modal', ['show']);
serviceMock = jasmine.createSpyObj('service', ['resetPassword']);
inject(function($rootScope, $controller, _$q_) {
$scope = $rootScope.$new();
$q = _$q_;
ctrl = $controller('MainCtrl', {
$scope: $scope,
modal: modalMock,
service: serviceMock
});
});
});
it('should reset the password when the user confirms', function() {
// Arrange
var deferred = $q.defer();
deferred.resolve('ok');
modalMock.show.and.returnValue(deferred.promise);
// Act
$scope.click();
$scope.$digest(); // Makes Angular resolve the promise
// Assert
expect(serviceMock.resetPassword).toHaveBeenCalled();
});
it('should not reset the password when the user cancels', function() {
// Arrange
var deferred = $q.defer();
deferred.resolve('cancel');
modalMock.show.and.returnValue(deferred.promise);
// Act
$scope.click();
$scope.$digest(); // Makes Angular resolve the promise
// Assert
expect(serviceMock.resetPassword).not.toHaveBeenCalled();
});
});
Working Plunker
That mock arrangement code within each test could be moved into a beforeEach section so it doesn't get duplicated. I didn't do that in order to make things simple.
Related
I would like to test my then and catch function from my $scope.customerinfo. The problem is i dont know how exactly.
var app = angular.module('shop', ['ngRoute','ngResource'])
.factory('Customerservice', function ($resource) {
return $resource('http://localhost:8080/Shop/:customer',{customer: "#customer"});
})
.controller('customerController', function ($scope,Customerservice) {
$scope.customerinfo = CustomerService.get({customer: "Mark"});
$scope.customerinfo.$promise.then(function(info) {
return info;
}).catch(function(errorResponse) {
throw errorResponse;
});
});
Im not done yet but this is my jasmine code
describe('Testing the customerinfo', function () {
var $scope;
var $q;
var deferred;
beforeEach(module('shop'));
beforeEach(inject(function($controller, _$rootScope_, _$q_) {
$q = _$q_;
$scope = _$rootScope_.$new();
deferred = _$q_.defer();
$controller('userController', {
$scope: $scope
});
}));
it('should reject promise', function () {
// I want to check if the catch option is working
});
});
So how exactly can i do this, or do i need to refactor the code?
The jasmine 'it' method takes a done parameter that you can call for async testing
it('Should reject', function(done) {
someAsyncFunction().catch(function(result) {
expect(result.status).toBe(401);
done();
});
});
I am trying to write the test cass for the factory which is returing a JSON response.
But I am getting the error:
Error: [$injector:unpr] http://errors.angularjs.org/1.4.1/$injector/unpr?p0=serviceProvider%20%3C-%20service
at Error (native)
Here is my code:
(function () {
angular.module('uspDeviceService',[]).factory('getDevice', GetDevice);
GetDevice.$inject = ['$http'];
function GetDevice($http) {
getDeviceList = function() {
return $http.get("static/test-json/devices/device-list.json");
}
return {
getDeviceList: getDeviceList
}
}
}());
Code for Test case:
describe('Get Product test', function() {
beforeEach(module('uspDeviceService'));
var service, httpBackend, getDevice ;
beforeEach(function () {
angular.mock.inject(function ($injector) {
//Injecting $http dependencies
httpBackend = $injector.get('$httpBackend');
service = $injector.get('service');
getDevice = $injector.get('getDevice');
})
});
console.log('Injection Dependencies is done');
describe('get Device List', function () {
it("should return a list of devices", inject(function () {
httpBackend.expectGET("static/test-json/devices/device-list.json").respond("Response found!");
httpBackend.flush();
}))
})
});
I am new to Angular Unit testing, can anyone please help me, where I am going wrong..
Two things that jump out at me:
Your angular.module declaration is defining a module, not getting the module. I would encourage you to split that up so that it's a fair bit more clear what your intent is.
angular.module('uspDeviceService', []);
angular.module('uspDeviceService').factory('getDevice', GetDevice);
It likely works as-is, but clarity is important.
What is...service? It's not defined anywhere in your code, and Angular can't find it either, hence the error message. You may be looking to get getDevice instead. Also, name your test variable with respect to what it actually is, so you don't confuse yourself.
// defined above
var getDevice;
// while injecting
getDevice = $injector.get('getDevice');
Supposing that you have an angularjs controller myController defined in myModule. The controller do some action when the api call is success and shows a flash message when api returns success = false. The your controller code would be something like
angular.module('myModule')
.controller( 'myController', function ( $scope,flashService, Api ) {
Api.get_list().$promise.then(function(data){
if(data.success) {
$scope.data = data.response
}
else{
flashService.createFlash(data.message, "danger");
}
});
});
Now to test both success = true and success = false we
describe('myController', function(){
var $rootScope, $httpBackend, controller, flashService;
var apilink = 'http://apilink';
beforeEach(module('myModule'));
beforeEach(inject(function(_$httpBackend_,_$rootScope_, _$controller_, _flashService_) {
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
flashService = _flashService_;
controller = _$controller_("myController", {$scope: $rootScope});
}));
it('init $scope.data when success = true', function(){
$httpBackend.whenGET(apilink)
.respond(
{
success: true,
response: {}
});
$httpBackend.flush();
expect($rootScope.data).toBeDefined();
});
it('show flash when api request failure', function(){
spyOn(flashService, 'createFlash');
$httpBackend.whenGET(apilink)
.respond(
{
success: false
});
$httpBackend.flush();
expect(flashService.createFlash).toHaveBeenCalled();
});
});
You are always going to mock the response because here we are testing the javascript code behaviour and we are not concerned with the Api. You can see when success the data is initialized and when success is false createFlash is called.
As far as test for factory is concerned you can do
describe('Get Product test', function() {
beforeEach(module('uspDeviceService'));
var service, httpBackend, getDevice ;
beforeEach(function () {
inject(function ($injector) {
httpBackend = $injector.get('$httpBackend');
service = $injector.get('service');
getDevice = $injector.get('getDevice');
});
});
describe('get Device List', function () {
it("should return a list of devices", inject(function () {
httpBackend.expectGET("static/test-json/devices/device- list.json").respond("Response found!");
var result = getDevice.getDeviceList();
httpBackend.flush();
expect(result).toEqual('Response found!');
}));
});
});
I am calling an API service which returns a promise from a factory.
Here is a part of my factory.
factories.factory('OnBoardingFactory', ['$http',
function ($http) {
var dataFactory = {};
dataFactory.get = function (url) {
return $http.get('http://localhost/api/onboarding/' + url)
};
return dataFactory
}
]);
And here is where its called from the controller:
OnBoardingFactory.get('login?username=test&password=password')
.then(function(response){
$scope.response = response.status;
})
This returns data in the controller absolutely fine. However I have difficulties when I come to test it. Here is my test script:
var scope, FakeOnBoardingFactory, controller, q, deferred;
beforeEach(module('app.module'));
beforeEach(function () {
FakeOnBoardingFactory = {
get: function () {
deferred = q.defer();
// Place the fake return object here
deferred.resolve({ response: {status: 200}});
return deferred.promise;
}
};
spyOn(FakeOnBoardingFactory, 'get').and.callThrough();
});
beforeEach(inject(function ($q, $rootScope, $controller, $injector ) {
scope = $rootScope.$new();
q = $q;
controller = $controller(OnBoardingCtrl, {
$scope: scope,
OnBoardingFactory: FakeOnBoardingFactory
})
}));
it('Should call the form and return 200', function () {
// Execute form
scope.loginCredentials({$valid: true});
scope.$apply();
// Ensure script is called (which passes fine)
expect(FakeOnBoardingFactory.get).toHaveBeenCalled();
scope.$apply();
// BREAKS HERE
expect(scope.status).toBe(200);
})
When expect(FakeOnBoardingFactory.get).toHaveBeenCalled(); is called, this passes fine. However then I run expect(scope.status).toBe(200), it breaks "Expected undefined to be 200".
This would indicate that my FakeOnBoardingFactory isn't returning any data. But I can't seem to find the issue.
It must be the change to support multiple body assertions that has caused this bug.
The workaround for now is to either don't use expect and do your assertion in the end function callback.
So instead of .expect(200) it would be.
end(function(err,res) { res.status.should.equal(200) },
or if you do use expect.. you need to make sure you specify a body as well as just a status..
it('should assert status only 1', function(done){
var app = express();
app.get('/user', function(req, res){
res.send(201, { name: 'tobi' }); }); request(app) .get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '20')
.expect(201)
.end(function(err, res){
if (err) throw err;
});
})
This is one of the functions in my controller
function sendMeetingInvitation(companyId, meetingId) {
meetingService.sendInvitations(companyId, meetingId)
.success(function() {
$state.go('company.meeting.view', {}, {
reload: true
});
})
.error(function() {
//more code for error handling
});
}
Below is the test case I'm using to test when we call the sendMeetingInvitation(), whether it should invoke the to the success() block of the service call of meetingService.sendInvitations
describe('EditMeetingCtrl.sendMeetingInvitation()', function() {
var $rootScope, scope, $controller, $q, companyService, meetingService;
var mockedHttpPromise = {
success: function() {}
};
beforeEach(angular.mock.module('MyApp'));
beforeEach(angular.mock.inject(function(_$httpBackend_, _companyService_, _meetingService_) {
$httpBackend = _$httpBackend_;
companyService = _companyService_;
meetingService = _meetingService_;
}));
beforeEach(inject(function($rootScope, $controller, _meetingService_) {
scope = $rootScope.$new();
createController = function() {
return $controller('EditMeetingCtrl', {
$scope: scope,
meeting: {},
meetingService: _meetingService_
});
};
var controller = new createController();
}));
it("should should send invitations", function() {
spyOn(meetingService, 'sendInvitations').and.returnValue(mockedHttpPromise);
scope.sendMeetingInvitations(123456, 123456);
expect(meetingService.sendInvitations).toHaveBeenCalledWith(123456, 123456);
});
});
I get this error which is not really helpful .
PhantomJS 1.9.8 (Windows 8) In EditMeetingCtrl EditMeetingCtrl.sendMeetingInvitation() should should send invitations FAILED
TypeError: 'undefined' is not an object (near '...})...')
What am I doing here wrong? I tried my mockedHttpPromise to following . but same result
var mockedHttpPromise = {
success: function() {},
error: function() {}
};
The function sendInvitations expects to return a promise, so, what you need to do is to create a defer and return it, like this:
-First of all you need to inject $q: $q = $injector.get('$q');
-Create a defer: deferred = $q.defer();
Your function mockedHttpPromise, should look like:
function mockedHttpPromise() {
deferred = $q.defer();
return deferred.promise;
}
And inside your test:
it("should should send invitations", function() {
spyOn(meetingService, 'sendInvitations').and.returnValue(mockedHttpPromise);
scope.sendMeetingInvitations(123456, 123456);
deferred.resolve({});
scope.$apply();
expect(meetingService.sendInvitations).toHaveBeenCalledWith(123456, 123456);
});
And to test error block, change deferred.resolve to deferred.reject
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