How to mock services in directive tests? - angularjs

i have a directive with the injected apiService which is a wrapper around $http and others custom services.
when I try to test the directive, I don't know how to mock the service:
app.directive('coreMenu', ['apiService', function (apiService) {
return {
restrict : 'E',
replace : true,
templateUrl : 'Directives/CoreMenu.tpl.html',
link: function (scope) {
apiService.get({
module: 'Core',
route: 'Menu'
}).then(function (response) {
scope.menuItems = response.data;
}, function (response) {
// error: no menu available
});
}
};
}]);
Test:
describe('coreMenu directive', function() {
var $compile, $rootScope;
beforeEach(function () {
module('AdminTechPortal');
module('CoreLeftMenu.tpl.html');
inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
});
it('replaces the element with the appropriate content', function() {
var element = $compile("<core-menu></core-menu>")($rootScope);
$rootScope.$digest();
expect(element.html()).toContain('id="core-menu"');
});
});
This test throws ( which is normal):
Error: Unexpected request: GET /Core/Menu/
Is it possible to mock apiService and not just the xhr call using $httpbackend ?

What I do in such cases follows... Bear in mind that testing promises (this way) has a bit of work and will fail if the promise needs to be chained!
describe('coreMenu directive', function() {
var $compile, $rootScope, apiServiceMock;
// BIG WARNING:
// This beforeEach() block MUST run before using any inject()
beforeEach(function () {
apiServiceMock = {};
apiServiceMock.get = jasmine.createSpy('apiServiceMock.get').and.callFake(function() {
return { // mock the promise
then: function(successCb, errorCb) {
// keep the callbacks to call them at our convenience
apiServiceMock._successCb = successCb;
apiServiceMock._errorCb = errorCb;
}
};
});
module(function($provide) {
// override the apiService in the DI container
$provide.value('apiService', apiServiceMock);
});
});
beforeEach(function () {
module('AdminTechPortal');
module('CoreLeftMenu.tpl.html');
inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
});
});
it('replaces the element with the appropriate content', function() {
var element = $compile("<core-menu></core-menu>")($rootScope);
$rootScope.$digest();
// SAMPLE TESTS
expect(apiServiceMock.get).toHaveBeenCalledWith(...);
// test success
apiServiceMock._successCb(...);
expect(...) // things to expect on success
// test failure - probably in another it()
apiServiceMock._errorCb(...);
expect(...) // things to expect on error
});
});

Follow these steps:
add $httpBackend variable:
var $compile, $rootScope, $httpBackend;
Inject and assign on beforeEach
inject(function (_$compile_, _$rootScope_, _$httpBackend_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
});
Create an afterEach
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
test
it('replaces the element with the appropriate content', function() {
$httpBackend.expectGET('/Core/Menu/');
var element = $compile("<core-menu></core-menu>")($rootScope);
$httpBackend.flush();
$rootScope.$digest(); // Not sure if still needed
expect(element.html()).toContain('id="core-menu"');
});

Related

promise testing in angular

I want to test the following method in my controller class:
// getIds() {
// this.api.getIds()
// .then((response)=> {
// this.ids = response.data;
// this.doSomethingElse();
// });
// }
I'm not sure how to handle the promise using jasmine and karma. The project is written in ES6. api.getIds() returns a $http.get().
beforeEach(function() {
inject(function($controller, $rootScope, _api_) {
vm = $controller('MainController', {
api: _api_,
$scope:$rootScope.$new()
});
});
});
beforeEach(function () {
vm.getIds();
});
it('should set the ids', function () {
expect(vm.ids).toBeDefined(); //error
});
How do I wait for the promise to complete before running the expect() ?
First of all, you should use the done callback provided by the jasmine; see async support in Jasmine.
Then, you should mock your getIds on the api so that it returns a resolved promise with an expected value. The asserts should be done after the then promise is called - se bellow the full example.
beforeEach(function () {
var $q, vm, api, $controller, $rootScope;
inject(function (_$controller_, _$rootScope_, _$q_) {
$q = _$q_;
$controller = _$controller_;
$rootScope = _$rootScope_;
api = jasmine.createSpyObj('api', ['getIds']);
api.getIds.and.returnValue($q.when([]));
vm = $controller('MainController', {
api: api,
$scope: $rootScope.$new()
});
});
});
it('should set the ids', function (done) {
vm
.getIds()
.then(function (ids) {
expect(ids).toBeDefined();
// add more asserts
done();
});
});
As a side note, if the this.doSomethingElse(); is a promise too, you have to return it in the first then so that you can test the final result.

Unit Test large number of injected services using Jasmine

I'm very new to the AngularJs unit testing with Jasmine.So could you tell me how can I test below mentioned controller and countyService.getAllCountiesAsync() method using Jasmine.Thanks in advance.
Note : The controller below is having more than 50 injected services (I have shown few below).So I don't know which method is good for mock those also ?
Controller :
(function () {
appModule.controller('myController', [
'$scope', '$modalInstance', 'abp.services.app.property', 'abp.services.app.county', 'abp.services.app.propertyClass', 'abp.services.app.schoolDistrict'
function ($scope, $modalInstance, propertyService, countyService, propertyClassService, schoolDistrictService) {
vm.getAllCounties = function () {
countyService.getAllCountiesAsync().success(function (result) {
vm.counties = result.items;
});
};
vm.getAllCounties();
} ]);
})();
WebApi method :
public async Task<ListResultOutput<CountyListDto>> GetAllCountiesAsync()
{
var counties = await _countyRepository
.GetAllListAsync();
return new ListResultOutput<CountyListDto>(counties.OrderBy(o => o.Name).MapTo<List<CountyListDto>>());
}
You should write test cases for service and controller.
For services 'Daan van Hulst' has already given answer and for controller see below code:
describe('service tests', function () {
var $compile,$controller,myController, $rootScope, propertyService, countyService, propertyClassService, schoolDistrictService;
//All module dependencies
beforeEach(module('your-app-name'));
//inject required services and _$controller_ to create controller
beforeEach(inject(function(_$compile_,_$controller_, _$rootScope_, _propertyService_, _countyService_, _propertyClassService_, _schoolDistrictService_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_; // This is IMP
countyService = _countyService_;
// remianig services
// Now create controller
myController = $controller('myController', {
$scope : scope,
propertyService : propertyService // all other services
});}
it('should test something', function() {
spyOn(countyService, 'getAllCountiesAsync').and.callFake(function () {
var d = q.defer();
d.resolve({ items: [{data:'somedata'}] });
return d.promise;
});
myController.getAllCounties();
expect(myController.counties).not.toBe(null);
});
Update
I might have made mistakes, but this is the idea:
describe('service tests', function () {
var $compile, $rootScope, scope, vm, propertyService, countyService, propertyClassService, schoolDistrictService;
beforeEach(module('your-app-name'));
beforeEach(inject(function(_$compile_, _$rootScope_, $controller, _propertyService_, _countyService_, _propertyClassService_, _schoolDistrictService_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
propertyService = _propertyService_;
countyService = _countyService_;
propertyClassService = _propertyClassService_;
schoolDistrictService = _schoolDistrictService_;
vm = $controller('myController', {'$scope': scope})
spyOn(countyService, "getAllCountiesAsync").and.callFake(function() {
var deferred = $q.defer();
deferred.resolve({data: [{id:0}]});
return deferred.promise;
});
}));
it('can do remote call', inject(function() {
//Arrange
result = [{id:0}];
// Act
vm.getAllCounties();
// Assert
expect(vm.counties).toBe(result); //assert to whatever is resolved in the spyOn function
});
});
}
I assume that you create Angular services for all your services and that you app is working. Then, you can inject them in your tests:
describe('service tests', function () {
var $compile, $rootScope, propertyService, countyService, propertyClassService, schoolDistrictService;
beforeEach(module('your-app-name'));
beforeEach(inject(function(_$compile_, _$rootScope_, _propertyService_, _countyService_, _propertyClassService_, _schoolDistrictService_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
propertyService = _propertyService_;
countyService = _countyService_;
propertyClassService = _propertyClassService_;
schoolDistrictService = _schoolDistrictService_;
}));
it('should test something', function() {
expect(propertyService).toBeDefined();
expect(countyService).toBeDefined();
expect(propertyClassService).toBeDefined();
expect(schoolDistrictService).toBeDefined();
});
});
Update
I accidentally posted my solution in the answer above, so corrected it now. You can create your controller with $controller and pass in a scope object. You can also pass in any other dependencies. Then create a spy on the service, and once it gets called, call a different function which resolves a promise with mock data:
describe('service tests', function () {
var $compile, $rootScope, scope, vm, propertyService, countyService, propertyClassService, schoolDistrictService;
beforeEach(module('your-app-name'));
beforeEach(inject(function(_$compile_, _$rootScope_, $controller, _propertyService_, _countyService_, _propertyClassService_, _schoolDistrictService_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
propertyService = _propertyService_;
countyService = _countyService_;
propertyClassService = _propertyClassService_;
schoolDistrictService = _schoolDistrictService_;
// Create the controller, and pass in the scope with possible variables that you want to mock.
vm = $controller('myController', {'$scope': scope})
//Create a spy on your getAllCountiesAsync function and make it return a mock promise with mock data.
spyOn(countyService, "getAllCountiesAsync").and.callFake(function() {
var deferred = $q.defer();
deferred.resolve({data: [{id:0}]});
return deferred.promise;
});
}));
it('can do remote call', inject(function() {
//Arrange
result = [{id:0}];
// Act
vm.getAllCounties();
//I think that you also have to do this, but I am not a 100% sure.
scope.$apply();
// Assert
expect(vm.counties).toBe(result); //assert to whatever is resolved in the spyOn function
});
});
}

How can I resolve a $q.all in my controller in a Karma unit test?

My controller has:
switchUserAccount: function() {
$scope.model.currentMode = 'user';
console.log(ipCookie('currentPatientId'));
$q.all([facilityCache.getFacility(), facilityGroupCache.getGroupList(), languageCache.getLanguageList(), genderCache.getGenderList(), raceCache.getRaceList(), dosingCache.getDosingOptions()])
.then(function(){
console.log('back from then');
cache.set('ui', 'adminPage', '');
cache.set('ui', 'schedulePage', 'patients');
if(ipCookie('currentPatientId')) {
$location.path('/patient/view/' + ipCookie('currentPatientId'));
} else {
$location.path('/patients');
}
});
},
and my test is
describe('MainHeaderController', function() {
var scope, $rootScope, $locationMock, $httpBackend;
beforeEach(function() {
module('mapApp');
return inject(function($injector) {
var $controller, $q, ipCookieMock;
$rootScope = $injector.get('$rootScope');
$controller = $injector.get('$controller');
$httpBackend = $injector.get('$httpBackend');
$q = $injector.get('$q');
$locationMock = jasmine.createSpyObj('$location', ['path'])
ipCookieMock = function() {
return 123;
}
scope = $rootScope.$new()
$controller('MainHeaderController', {
$scope: scope,
$location: $locationMock,
$q: $q,
ipCookie: ipCookieMock
});
$httpBackend.whenGET('/rpc/session').respond(200);
$httpBackend.whenPOST('/rpc').respond(200);
return scope.$digest();
});
});
it('should redirect to a patient view if a cookie is set', function($rootScope) {
scope.switchUserAccount();
// $rootScope.$apply();
expect($locationMock.path).toHaveBeenCalledWith('/patient/view/123');
});
});
So what I expect to happen is for $location.path to be called with /patient/view/123. Instead, what I get is
Expected spy $location.path to have been called with [ '/patient/view/123' ] but actual calls were [ ].
If I uncomment out the $rootScope.$apply(), I get
TypeError: 'undefined' is not a function (evaluating '$rootScope.$apply()')
So how can I trigged the $q.all in my controller so that the test can pass properly?
Thanks!
You're hiding the $rootScope variable of your test suite by declaring it as an argument of your test function. That's why it's undefined: jasmine calls the test functions withput any argument.
Replace
it('should redirect to a patient view if a cookie is set', function($rootScope) {
by
it('should redirect to a patient view if a cookie is set', function() {

How to test services in an AngularJS Controller?

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.

How can we test non-scope angular controller methods?

We have few methods in Angular Controller, which are not on the scope variable.
Does anyone know, how we can execute or call those methods inside Jasmine tests?
Here is the main code.
var testController = TestModule.controller('testController', function($scope, testService)
{
function handleSuccessOfAPI(data) {
if (angular.isObject(data))
{
$scope.testData = data;
}
}
function handleFailureOfAPI(status) {
console.log("handleFailureOfAPIexecuted :: status :: "+status);
}
// this is controller initialize function.
function init() {
$scope.testData = null;
// partial URL
$scope.strPartialTestURL = "partials/testView.html;
// send test http request
testService.getTestDataFromServer('testURI', handleSuccessOfAPI, handleFailureOfAPI);
}
init();
}
Now in my jasmine test, we are passing "handleSuccessOfAPI" and "handleFailureOfAPI" method, but these are undefined.
Here is jasmine test code.
describe('Unit Test :: Test Controller', function() {
var scope;
var testController;
var httpBackend;
var testService;
beforeEach( function() {
module('test-angular-angular');
inject(function($httpBackend, _testService_, $controller, $rootScope) {
httpBackend = $httpBackend;
testService= _testService_;
scope = $rootScope.$new();
testController= $controller('testController', { $scope: scope, testService: testService});
});
});
afterEach(function() {
httpBackend.verifyNoOutstandingExpectation();
httpBackend.verifyNoOutstandingRequest();
});
it('Test controller data', function (){
var URL = 'test server url';
// set up some data for the http call to return and test later.
var returnData = { excited: true };
// create expectation
httpBackend.expectGET(URL ).respond(200, returnData);
// make the call.
testService.getTestDataFromServer(URL , handleSuccessOfAPI, handleFailureOfAPI);
$scope.$apply(function() {
$scope.runTest();
});
// flush the backend to "execute" the request to do the expectedGET assertion.
httpBackend.flush();
// check the result.
// (after Angular 1.2.5: be sure to use `toEqual` and not `toBe`
// as the object will be a copy and not the same instance.)
expect(scope.testData ).not.toBe(null);
});
});
I know this is an old case but here is the solution I am using.
Use the 'this' of your controller
.controller('newController',['$scope',function($scope){
var $this = this;
$this.testMe = function(val){
$scope.myVal = parseInt(val)+1;
}
}]);
Here is the test:
describe('newDir', function(){
var svc,
$rootScope,
$scope,
$controller,
ctrl;
beforeEach(function () {
module('myMod');
});
beforeEach(function () {
inject(function ( _$controller_,_$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$compile = _$compile_;
$scope = $rootScope.$new();
ctrl = $controller('newController', {'$rootScope': $rootScope, '$scope': $scope });
});
});
it('testMe inc number', function() {
ctrl.testMe(10)
expect($scope.myVal).toEqual(11);
});
});
Full Code Example
As is you won't have access to those functions. When you define a named JS function it's the same as if you were to say
var handleSuccessOfAPI = function(){};
In which case it would be pretty clear to see that the var is only in the scope within the block and there is no external reference to it from the wrapping controller.
Any function which could be called discretely (and therefore tested) will be available on the $scope of the controller.

Resources