I have a simple enough function that closes an $mdSidenav instance in my application
function closeSideNav() {
$mdSidenav('left').close();
}
I'm now needing to unit test this, but am having trouble writing an expectation for the close() call on $mdSidenav.
I thought about using $provide in my test spec
module(function($provide) {
$provide.value('$mdSidenav', function(id) {
return {
close: jasmine.createSpy('$mdSidenav.close')
}
})
});
beforeEach(inject(function(_$controller_, _$mdSidenav_) {
$controller = _$controller_;
$mdSidenav = _$mdSidenav_;
}));
beforeEach(function() {
vm = $controller('NavbarController', {
$mdSidenav: $mdSidenav
});
});
describe('vm.closeSideNav', function() {
beforeEach(function() {
spyOn($mdSidenav, 'close');
vm.closeSideNav()
});
it('should call $mdSidenav.close()', function() {
expect($mdSidenav.close).toHaveBeenCalled();
});
});
This throws a couple of errors:
Error: close() method does not exist
Error: Expected a spy, but got undefined.
Has anyone managed to mock out $mdSidenav and offer me some guidance please?
Thanks
UPDATE
Based on the suggested answer, I have now updated my test spec to
'use strict';
describe('NavbarController', function() {
var $controller,
vm,
$mdSidenav,
sideNavCloseMock;
beforeEach(function() {
module('app.layout');
sideNavCloseMock = jasmine.createSpy();
module(function($provide) {
$provide.value('$mdSidenav', function() {
return function(sideNavId) {
return {close: sideNavCloseMock}
}
})
});
});
beforeEach(inject(function(_$controller_, _$mdSidenav_) {
$controller = _$controller_;
$mdSidenav = _$mdSidenav_;
}));
beforeEach(function() {
vm = $controller('NavbarController', {
$mdSidenav: $mdSidenav
});
});
describe('vm.closeSideNav', function() {
beforeEach(function() {
vm.closeSideNav()
});
it('should call $mdSidenav.close()', function() {
expect(sideNavCloseMock).toHaveBeenCalled();
});
});
});
And for a sanity check, my actual controller looks as follows:
(function () {
'use strict';
angular
.module('app.layout')
.controller('NavbarController', Controller);
Controller.$inject = ['$mdSidenav'];
function Controller($mdSidenav) {
var vm = this;
vm.closeSideNav = closeSideNav;
//This only affects the sideNav when its not locked into position, so only on small\medium screens
function closeSideNav() {
$mdSidenav('left').close();
}
}
})();
Unfortunately this still isn't working for me, and I end up with a different error
TypeError: undefined is not a constructor (evaluating '$mdSidenav('left').close())
close method doesn't belong to $mdSidenav. $mdSidenav is a function that returns a side nav object. That's why it complains 'close() method does not exist'.
What you can do is mock the $mdSidenav to return an object hat has mocked close method, like this: -
var sideNavCloseMock;
beforeEach(module(function($provide){
sideNavCloseMock = jasmine.createSpy();
$provide.factory('$mdSidenav', function() {
return function(sideNavId){
return {close: sideNavCloseMock};
};
});
}));
then do
it('should call $mdSidenav.close()', function() {
expect(sideNavCloseMock).toHaveBeenCalled();
});
Related
I'm adding unit tests to my AngularJS application, and running into an issue where the controller I'm testing is aware of my mocks (logs the correct mock object), but my unit tests cannot return the mock value. I've been stuck on this one for a while.
Mock Service
angular.module('mocks.myService', [])
.factory('myService', function() {
var service = {
hi : 'hi'
};
return service;
});
Controller
.controller('MyCtrl', ['myService', function(myService) {
var vm = this;
vm.doThing = function() {
console.log(myService);
// Object{hi:'hi'}
};
}]);
Unit Test
describe('myApp.selection module', function() {
var myCtrl, myService;
beforeEach(function() {
myService = module('mocks.myService');
console.log(myService);
// undefined
});
describe('controller', function(){
it('should exist', inject(function($controller) {
myCtrl = $controller('MyCtrl');
expect(myCtrl).toBeDefined();
}));
it ('should do thing', function() {
myCtrl.doThing();
});
});
});
Try this:
describe('myApp.selection module', function () {
var myCtrl, myService;
beforeEach(module("mocks.myService"));
beforeEach(inject(function (_myService_) {
myService = _myService_;
console.log(myService);
}));
describe('controller', function () {
it('should exist', inject(function ($controller) {
myCtrl = $controller('MyCtrl');
expect(myCtrl).toBeDefined();
}));
it('should do thing', function () {
myCtrl.doThing();
});
});
});
source: https://docs.angularjs.org/guide/unit-testing
So Im trying to figure out how to write unit tests for my angular controller. I am using karma as my runner. I was able to write 1 successful test but every time I try to write another test it yells at me about unexpected calls and such.
Here is my controller im trying to test.
(function (angular) {
'use strict';
var ngModule = angular.module('myApp.dashboardCtrl', []);
ngModule.controller('dashboardCtrl', function ($scope, $http) {
//"Global Variables"
var vm = this;
vm.success = false;
vm.repos = [];
//"Global Functions"
vm.addRepository = addRepository;
vm.listRepos = listRepos;
//Anything that needs to be instantiated on page load goes in the init
function init() {
listRepos();
}
init();
// Add a repository
function addRepository(repoUrl) {
$http.post("/api/repo/" + encodeURIComponent(repoUrl)).then(function (){
vm.success = true;
vm.addedRepo = vm.repoUrl;
vm.repoUrl = '';
listRepos();
});
}
//Lists all repos
function listRepos() {
$http.get('/api/repo').then( function (response){
vm.repos = response.data;
});
}
});
}(window.angular));
So I have a test written for listRepos(). It goes as follows
describe('dashboardCtrl', function() {
var scope, httpBackend, createController;
// Set up the module
beforeEach(module('myApp'));
beforeEach(inject(function($rootScope, $httpBackend, $controller) {
httpBackend = $httpBackend;
scope = $rootScope.$new();
createController = function() {
return $controller('dashboardCtrl', {
'$scope': scope
});
};
}));
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('should call listRepos and return all repos from the database', function() {
var controller = createController();
var expectedResponse = [{id: 12345, url: "https://github.com/myuser/myrepo.git"}];
httpBackend.expect('GET', '/api/repo')
.respond(expectedResponse);
httpBackend.flush();
scope.$apply(function() {
scope.listRepos;
});
expect(controller.repos).toEqual(expectedResponse);
});
This works and the test passes. Now my problem is I want to write another test to test the other function that calls a new api endpoint.
This is the test im trying to write for addRepository.
it('should addRepository to the database', function() {
var controller = createController();
var givenURL = "https://github.com/myuser/myURLtoMyRepo.git";
httpBackend.expect('POST', '/api/repo/' + encodeURIComponent(givenURL)).respond('success');
httpBackend.flush();
scope.$apply(function() {
scope.addRepository(givenURL);
});
expect(controller.success).toBe(true);
expect(controller.listRepos).toHaveBeenCalled();
});
The error I get when I add this test to the spec is:
Error: Unexpected request: GET /api/repo
Expected POST /api/repo/https%3A%2F%2Fgithub.com%2Fmyuser%2FmyURLtoMyRepo.git
at $httpBackend
Error: [$rootScope:inprog] $digest already in progress
http://errors.angularjs.org/1.4.8/$rootScope/inprog?p0=%24digest
The example I am working with is this one here
Any suggestions or tips is greatly appreciated!
UPDATE:
So changed my function to return the promise from the $http.post,
I rewrote my 2nd test and also wrapped my first test in a describe block describing the function its trying to test.
With the following:
describe('addRepository', function () {
it('should addRepository to the database', function () {
var controller = createController();
var givenURL = "https://github.com/myuser/myURLtoMyRepo.git";
httpBackend.expect('POST', '/api/repo/' + encodeURIComponent(givenURL)).respond('success');
scope.$apply(function () {
scope.addRepository(givenURL);
});
httpBackend.flush();
expect(controller.success).toBe(true);
});
it('should call listRepos', function() {
var controller = createController();
httpBackend.expect('GET', '/api/repo').respond('success');
controller.controller().then(function (result) {
expect(controller.listRepos).toHaveBeenCalled();
});
httpBackend.flush();
});
});
I still get the error:
Error: Unexpected request: GET /api/repo
Expected POST /api/repo/https%3A%2F%2Fgithub.com%2Fmyuser%2FmyURLtoMyRepo.git
at $httpBackend
Error: [$rootScope:inprog] $digest already in progress
but also
TypeError: 'undefined' is not a function (evaluating 'controller.controller()')
Error: Unflushed requests: 1
which shows 2 tests failed.
The flush should come after the call to the function. I'd also change the function to return the promise from the $http.post:
// Add a repository
function addRepository(repoUrl) {
return $http.post("/api/repo/" + encodeURIComponent(repoUrl)).then(function (){
vm.success = true;
vm.addedRepo = vm.repoUrl;
vm.repoUrl = '';
listRepos();
});
}
And then in the test you can call it and test the success part:
EDIT
I changed the controller.controller() to what you have.
it('should call listRepos', function() {
// Your setup
ctrl.addRepository().then(function(result) {
expect(ctrl.listRepos).toHaveBeenCalled();
});
});
EDIT 2
I emulated as best i could your code and the tests I write for the code:
(function () {
'use strict';
angular
.module('myApp')
.controller('DashboardController',DashboardController);
DashboardController.$inject = ['$http'];
function DashboardController($http) {
var vm = this;
vm.success = false;
vm.repos = [];
vm.addRepository = addRepository;
vm.listRepos = listRepos;
init();
// Anything that needs to be instantiated on page load goes in the init
function init() {
vm.listRepos();
}
// Add a repository
function addRepository(repoUrl) {
return $http.post('http://jsonplaceholder.typicode.com/posts/1.json').then(function (){
vm.success = true;
vm.addedRepo = vm.repoUrl;
vm.repoUrl = '';
vm.listRepos();
});
}
// Lists all repos
function listRepos() {
return $http.get('http://jsonplaceholder.typicode.com/posts/1').then( function (response){
vm.repos = response.data;
});
}
};
}());
Here I'm using an online JSONPlaceholder API to simulate HTTP calls as I, obviously, can't hit what you're pointing at. And for the test (which all pass):
(function() {
'use strict';
fdescribe('DashBoardController', function() {
var $rootScope, scope, ctrl, $httpBackend;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$httpBackend_,$controller) {
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$httpBackend =_$httpBackend_;
ctrl = $controller('DashBoardController',{
$scope: scope
});
}));
beforeEach(function() {
// Setup spies
spyOn(ctrl,'listRepos');
});
describe('controller', function() {
it('should be defined', function() {
expect(ctrl).toBeDefined();
});
it('should initialize variables', function() {
expect(ctrl.success).toBe(false);
expect(ctrl.repos.length).toBe(0);
});
});
describe('init', function() {
it('should call listRepos', function() {
$httpBackend.expectGET('http://jsonplaceholder.typicode.com/posts/1')
.respond({success: '202'});
$httpBackend.expectPOST('http://jsonplaceholder.typicode.com/posts/1.json')
.respond({success: '202'});
ctrl.addRepository().then(function(result) {
expect(ctrl.success).toBe(true);
expect(ctrl.repoUrl).toBe('');
expect(ctrl.listRepos).toHaveBeenCalled();
});
$httpBackend.flush();
});
});
});
}());
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
The following test passes whether my expect statement is
expect(propertyFactory.readProperty).should.have.been.called;
or
expect(propertyFactory.readProperty).should.have.not.been.called;
I've tried this with or without the sinon stub.
describe('pf.propertyModule', function () {
var controller;
var propertyFactory;
beforeEach(module('pf.core'));
beforeEach(module('pf.propertyModule'));
beforeEach(inject(function(_propertyFactory_) {
propertyFactory = _propertyFactory_;
/*
sinon.stub(propertyFactory, 'readProperty', function() {
return {
data: {
property: 'xyz'
}
}
});*/
}));
describe('readPropertyCtrl', function () {
it('should have a passing first test', function () {
expect(propertyFactory.readProperty).should.have.been.called;
});
});
});
UPDATE: Ok, so I got past the initial "non-error" by ensuring "sinon-chai" was installed, but now I'm getting the output: thrown: [Function] } is not a spy or a call to a spy!
UPDATE: Ok, so I got the previous error working by calling sinon.spy(). However, now I'm wondering how one would go about mocking a method that is expected to return a promise. I'm getting the error " TypeError: 'undefined' is not an object (evaluating 'propertyFactory.readProperty({id: $stateParams.id}).then')
"
Updated code:
describe('pf.propertyModule', function () {
var ctrl;
var scope;
var propertyFactory;
beforeEach(module('pf.core'));
beforeEach(module('pf.propertyModule'));
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
propertyFactory = {
readProperty: sinon.spy()
};
ctrl = $controller('readPropertyCtrl as vm', {
$scope: scope,
propertyFactory: propertyFactory
});
}));
describe('readPropertyCtrl', function () {
it('should have a passing first test', function () {
expect(scope.vm).to.be.defined;
expect(propertyFactory.readProperty).should.have.been.called;
});
});
});
Code to test:
propertyFactory.readProperty({id: $stateParams.id}).then(getPropertySuccess);
function getPropertySuccess(response) {
vm.loadedProperty = response.data.property;
}
Updated Code:
describe('pf.propertyModule', function () {
var ctrl;
var scope;
var propertyFactory;
beforeEach(module('pf.core'));
beforeEach(module('pf.propertyModule'));
beforeEach(inject(function($rootScope, $controller, $q, _propertyFactory_) {
scope = $rootScope.$new();
propertyFactory = _propertyFactory_;
var deferred = $q.defer();
var promise = deferred.promise;
sinon.stub(propertyFactory, "readProperty").returns(promise);
deferred.resolve({property: 'xyz'});
ctrl = $controller('readPropertyCtrl as vm', {
$scope: scope,
propertyFactory: propertyFactory
});
}));
describe('readPropertyCtrl', function () {
it('should have a passing first test', function () {
expect(scope.vm).to.be.defined;
expect(propertyFactory.readProperty).should.have.been.called;
});
});
});
This attempt is returning thrown: [Function] } is not a spy or a call to a spy!
We're unit testing our services and facing issue spying on methods with arguments of dependent services.
I am writing unit tests for ServiceA
ServiceA.js
angular.module("App").service("ServiceA", function($http, ServiceB) {
this.detail = null;
this.method = function(id){
var sevrB = new ServiceB();
return sevrB.getId(1).then(function(response) {
this.detail = response.data;
});
};
});
ServiceB.js (is a factory)
(function () {
var dependencies = [
'../module'
];
define(dependencies, function (module) {
return module.factory('ServiceB', function ($http) {
var ServiceB= function () {
this.id = null;
};
ServiceB.prototype.getId = function(Id) {
return $http.get('/test/');
}
}
}());
Unit test code
describe('Testing ServiceA', function () {
var serviceA, serviceBMock;
beforeEach(function () {
var _serviceBMock = function () {
return {
getId:function(id){
return 'test';
}
};
};
angular.module('ServiceAMocks', [])
.value('ServiceB', _serviceBMock);
});
beforeEach(module('ServiceAMocks'));
beforeEach(inject(function (_ServiceA_, _ServiceB_) {
serviceA=_ServiceA_;
serviceBMock=_ServiceB_;
});
it('retrive Id', function () {
spyOn(serviceBMock,'getId').and.Return('test');
serviceA.method(1);
});
});
I am spying on getId method of ServiceB from ServiceA and if i mocked ServiceB as function i am getting error below
Error: getId() method does not exist
at jasmineInterface.spyOn
If I mock serviceB as object then i get error as
TypeError: object is not a function
var _serviceBMock = {
getId:function(id){
return 'test';
}
}
And I am not sure of testing promise in this scenario.
This version supports Jasmine 1.3
I’m injecting $q service as ServiceB wants to call method then. We can even go forward and resolve returned promise, but this is next step in testing.
Answer to previous version of question, where AngularJS injects instance of serviceB
describe('ServiceA', function () {
var serviceA, ServiceB, $q;
beforeEach(function () {
module('App');
});
beforeEach(function () {
module(function ($provide) {
$provide.value('ServiceB', {
getId: jasmine.createSpy('ServiceB.getId').andCallFake(function () {
return $q.all();
})
});
});
});
beforeEach(inject(function (_ServiceA_, _ServiceB_, _$q_) {
$q = _$q_;
serviceA = _ServiceA_;
ServiceB = _ServiceB_;
}));
describe('.method()', function () {
it('returns ServiceB.getId() argument', function () {
serviceA.method(1);
expect(ServiceB.getId).toHaveBeenCalledWith(1);
});
});
});
jsfiddle: http://jsfiddle.net/krzysztof_safjanowski/sDh35/