Injecting a mock provider into an angular unit test - javascript

I'm trying to figure out how to mock an angular provider for a unit test. In the following snippet I have a 'translate' provider that is used to determine which language will be displayed in a view by default. I'd like to inject a different version of this provider into my tests to ensure my app displays the right thing based on a provider's settings. What I'm doing right now clearly doesn't seem to be working. Thanks in advance for your help.
By the way, if you're wondering why a provider was used instead of something else like a service or a simple value, this is a contrived example that distills a problem I'm having in a larger application. I need to inject something into an application's config method, which means I need to mock a provider.
var app = angular.module('app', []);
app.config(function($provide) {
$provide.provider('translate', function() {
return {
$get: function() {
return {
language: 'en'
};
}
};
});
});
app.controller('ctl', function($scope, translate) {
if (translate.language === 'en') {
$scope.greeting = "Welcome to the application.";
} else {
$scope.greeting = "Velkommen til appen.";
}
});
// ---SPECS-------------------------
describe('App', function() {
beforeEach(angular.mock.module('app'));
describe('by default', function() {
beforeEach(angular.mock.inject(
function(_$compile_, _$rootScope_) {
const viewHtml = $('#view');
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.isOn = false;
elm = $(viewHtml);
$compile(elm)($rootScope);
$rootScope.$digest();
}));
it('shows English', function() {
expect(elm.text()).toMatch(/Welcome/);
});
});
describe('without English specified', function() {
beforeEach(angular.mock.module('app', function ($provide) {
$provide.provider('translate', function () {
return {
$get: function () {
return { preferredLanguage: 'no' };
}
};
});
}));
beforeEach(angular.mock.inject(
function(_$compile_, _$rootScope_) {
const viewHtml = $('#view');
$compile = _$compile_;
$rootScope = _$rootScope_;
$rootScope.isOn = false;
elm = $(viewHtml);
$compile(elm)($rootScope);
$rootScope.$digest();
}));
it('shows Norwegian', function() {
expect(elm.text()).toMatch(/Velkommen/);
});
});
});
// --- Runner -------------------------
(function() {
var jasmineEnv = jasmine.getEnv();
jasmineEnv.updateInterval = 1000;
var htmlReporter = new jasmine.HtmlReporter();
jasmineEnv.addReporter(htmlReporter);
jasmineEnv.specFilter = function(spec) {
return htmlReporter.specFilter(spec);
};
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
execJasmine();
};
function execJasmine() {
jasmineEnv.execute();
}
})();
<link href="http://jasmine.github.io/1.3/lib/jasmine.css" rel="stylesheet"/>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="http://code.angularjs.org/1.4.9/angular.js"></script>
<script src="http://code.angularjs.org/1.4.9/angular-mocks.js"></script>
<script src="http://jasmine.github.io/1.3/lib/jasmine.js"></script>
<script src="http://jasmine.github.io/1.3/lib/jasmine-html.js"></script>
<div ng-app="app">
<div id="view" ng-controller="ctl">{{greeting}}</div>
</div>

you can do it like this: -
beforeEach(module('app', function ($provide) {
$provide.provider('translate', function() {
return {
$get: function() {
return {
language: 'fr'
};
}
};
});
}));
you can also put the above code in a util method, that will take the language code as a parameter, so you don't spread above code everywhere.

Related

component service not being injected into test in jasmine

Given the following test.
The $provided service is not being injected. If I debug the test in karma I can see that the service being provided is the real one, and not the mock.
The really weird thing, is that if I remove the $provide.service... I get an error Error: [$injector:unpr] Unknown provider: ficaServiceProvider <- ficaService. This clearly means that the service is getting registered, just not replaced?
describe("component: FicaStatusComponent",
function () {
var fs;
beforeEach(function () {
module("aureus",
function ($provide) {
$provide.service("ficaService", function () {
this.status = function () {
return $q(function (resolve, reject) {
resolve([{ documentType: { id: 1 } }]);
});
}
})
});
});
beforeEach(inject(function (_$componentController_, _ficaService_) {
$componentController = _$componentController_;
fs = _ficaService_;
}));
it("should expose a `fica` object", function () {
console.log('should expose');
var bindings = {};
var ctrl = $componentController("ficaStatus", null, bindings);
expect(ctrl.fica).toBeDefined();
});
it("compliant with no documents should not be compliant",
function () {
var ctrl = $componentController("ficaStatus");
expect(ctrl.fica.length).toEqual(1);
});
}
);
The second test compliant with no documents... is failing.
Chrome 56.0.2924 (Windows 10 0.0.0) component: FicaStatusComponent compliant with no documents should not be compliant FAILED
Error: Unexpected request: GET api/fica/status/
I have also tried this, expecting to have an empty object injected, but the "real" service is there nevertheless?
module("aureus", function($provide) {
$provide.value("ficaService", function() { return {}; });
$provide.service("ficaService", function() { return {}; });
});
Here is the implementation of the controller for the component:
var FicaStatusController = (function () {
function FicaStatusController($log, $loc, ficaService) {
var _this = this;
this.$log = $log;
this.$loc = $loc;
this.ficaService = ficaService;
this.fica = [];
this.ficaService.status(1234).then(function (_) { return _this.fica = _; });
}
FicaStatusController.$inject = ["$log", "$location", "IFicaStatusService"];
module("aureus").component("ficaStatus", new FicaStatusComponent());
module("aureus").service("IFicaStatusService", FicaStatusService);
The service is as follows:
var FicaStatusService = (function () {
function FicaStatusService($log, $http) {
this.$log = $log;
this.$http = $http;
}
FicaStatusService.prototype.status = function (accountNumber) {
var url = "api/fica/status/" + accountNumber;
this.$log.log("status: " + url);
return this.$http
.get(url)
.then(function (_) { return _.data; });
};
return FicaStatusService;
}());
...
You have added your service in your module like this:
module("aureus").service("IFicaStatusService", FicaStatusService);
That means that you will need to provide IFicaStatusService instead of ficaService with $provide.

AngularJS 1.x Component testing with Karma resolve promise to test success function (spyOn)

I'm lost. I try to test if state.go is called after a ApiServiceMock has resolved a promise after a fake login.
For this test I get:
Expected spy go to have been called.
If I test another function which is directly triggering state.go it works. So I guess the promise isn't being resolved in the first place.
Using Angular Components and testing is quite new to me. Would be great if someone could give me a hint what I'm doing wrong or let me know if the whole approach is wrong in the first place.
login.component.ctrl.js
module.exports = function LoginComponentCtrl($state, $log, apiService) {
var vm = this;
vm.submit = function() {
apiService
.login(vm.username, vm.password)
.then(function(response) {
$state.go('storeDetail');
})
.catch(function(errData) {
$log.error(errData);
});
};
};
login.spec.js
var app = require('../../app.js');
var login = require('./login.module.js');
describe('login', function() {
var controller;
var element;
var scope;
var state;
var ApiServiceMock;
var StateMock;
var deferred;
beforeEach(angular.mock.module(app));
beforeEach(angular.mock.module(login));
describe('Component: login', function () {
beforeEach(inject(function($rootScope, $compile, $componentController, $q){
deferred = $q.defer();
ApiServiceMock = {
login: function () {
return deferred.promise;
}
};
StateMock = {
go: function () {
return true;
}
};
scope = $rootScope.$new();
controller = $componentController('login', {
$scope: scope,
apiService: ApiServiceMock,
$state: StateMock
});
element = angular.element('<login></login>');
element = $compile(element)(scope);
scope.$apply();
}));
it('on successful login', function() {
controller.username = 'Michael Jackson';
controller.password = 'dangerous123';
spyOn(StateMock, 'go').and.callThrough();
controller.submit();
deferred.resolve();
scope.$digest();
expect(StateMock.go).toHaveBeenCalled();
}););
});
});
Let me know if I can add further information to make this clearer.
Just directly return resolved promise from your mock service.
Try this
beforeEach(inject(function($rootScope, $compile, $componentController, $q){
ApiServiceMock = {
login: function () {
return $q.resolve({}); // if angular version is 1.4+
//return $q.when({}); // if angular less than 1.4
}
};
StateMock = {
go: function () {
return true;
}
};
scope = $rootScope.$new();
controller = $componentController('login', {
$scope: scope,
apiService: ApiServiceMock,
$state: StateMock
});
element = angular.element('<login></login>');
element = $compile(element)(scope);
scope.$apply();
}));
it('on successful login', function() {
controller.username = 'Michael Jackson';
controller.password = 'dangerous123';
spyOn(StateMock, 'go').and.callThrough();
controller.submit();
scope.$digest();
expect(StateMock.go).toHaveBeenCalled();
});
);

Mock factory on controller test

this is my controller:
angular
.module('studentsApp')
.controller('StudentsController', StudentsController);
function StudentsController($scope, StudentsFactory) {
$scope.students = [];
$scope.specificStudent= {};
var getStudents = function() {
StudentsFactory.getStudents().then(function(response) {
if($scope.students.length > 0){
$scope.students = [];
}
$scope.students.push(response.data);
});
};
}
This is my factory:
angular.module('studentsApp')
.factory('StudentsFactory', function($http) {
var base_url = 'http://localhost:3000';
var studentsURI = '/students';
var studentURI = '/student';
var config = {
headers: {
'Content-Type': 'application/json'
}
};
return {
getStudents: function() {
return $http.get(base_url + studentsURI);
}
};
});
And here is how I'm trying to unit test the controller:
describe('Controller: Students', function() {
var StudentsController, scope, StudentsFactory;
beforeEach(function() {
module('studentsApp');
inject(function($rootScope, $controller, $httpBackend, $injector) {
scope = $rootScope.$new();
httpBackend = $injector.get('$httpBackend');
StudentsFactory = $injector.get('StudentsFactory');
StudentsController = $controller('StudentsController', {
$scope : scope,
'StudentsFactory' : StudentsFactory
});
students = [{
name: 'Pedro',
age: 10
}, {
name: 'João',
age: 11
}, {
name: 'Thiago',
age: 9
}];
spyOn(StudentsFactory, 'getStudents').and.returnValue(students);
});
});
it('Should get all students', function() {
scope.students = [];
StudentsController.getStudents();
$scope.$apply();
expect(scope.students.length).toBe(3);
});
});
The problem is when I run the test, the following message is displayed:
undefined is not a constructor (evaluating
'StudentsController.getStudents()')
I looked at the whole internet trying to find a tutorial that can help me on that, but I didn't find anything, could someone help me here?
It's link to the fact that the function getStudent() is private (declared by var). Thus your test can't access it. You have to attach it to the $scope or this to be able to test it.
I generally use this in controller:
var $this = this;
$this.getStudents = function() {
...
};
There's no StudentsController.getStudents method. It should be
this.getStudents = function () { ... };
Mocked StudentsFactory.getStudents returns a plain object, while it is expected to return a promise.
$controller shouldn't be provided with real StudentsFactory service as local dependency (it is already provided with it by default):
var mockedStudentsFactory = {
getStudents: jasmine.createSpy().and.returnValue($q.resolve(students))
};
StudentsController = $controller('StudentsController', {
$scope : scope,
StudentsFactory : mockedStudentsFactory
});

How to test a controller function calling a private function that in turn calls a asynchronous function in angularjs

[plunkr][1]http://plnkr.co/edit/Jk1Rp3nEgUQTmDOs3xBl?p=preview
My current code is structured as below.
angular.module("app",[])
.service("dataService",function($http){
this.get = function (url) {
return $http.get(url);
};
})
.service("mainService",function(dataService){
this.getData = function(pattern){
return dataService.get(pattern+"/abc");
}
})
.controller("mainController",function($scope,mainService){
$scope.refreshData = function(pattern){
loadData(pattern);
}
function loadData(pattern){
mainService.getData(pattern)
.success(function(data){
console.log(data);
})
.error(function(error){
console.log(error);
})
}
})
I have been trying to make sense of how to test it by reading blogs but each blog has either a different approach or the blog is 2-3 years old. I would like to know how do I test the controller?
Should I test each function? If yes, then how should I test the private function? Is using the private function a good idea or should I just add the private function code to the scoped function?
Also is there any better way to do write this function?
Most important part where we are going to create stub:
beforeEach(function() {
var $httpResponse = {
success: function() {
return $httpResponse;
},
error: function() {
return $httpResponse;
}
};
var _stubMainService_ = {
getData: jasmine.createSpy('getData').and.returnValue($httpResponse)
};
angular.module('app')
.value('mainService', _stubMainService_);
});
and test that uses it:
it('rereshes data', function() {
var pattern = 'abcde';
scope.refreshData(pattern);
expect(mainService.getData).toHaveBeenCalledWith(pattern);
});
angular.module("app", [])
.service("dataService", function($http) {
this.get = function(url) {
return $http.get(url);
};
})
.service("mainService", function(dataService) {
this.getData = function(pattern) {
return dataService.get(pattern + "/abc");
}
})
.controller("mainController", function($scope, mainService) {
$scope.refreshData = function(pattern) {
loadData(pattern);
}
function loadData(pattern) {
mainService.getData(pattern)
.success(function(data) {
console.log(data);
}).error(function(error) {
console.log(error);
})
}
})
describe('mainController()', function() {
var scope, controller, mainService, $q;
beforeEach(module('app'));
beforeEach(function() {
var $httpResponse = {
success: function() {
return $httpResponse;
},
error: function() {
return $httpResponse;
}
};
var _stubMainService_ = {
getData: jasmine.createSpy('getData').and.returnValue($httpResponse)
};
angular.module('app')
.value('mainService', _stubMainService_);
});
beforeEach(inject(function($controller, $rootScope, _mainService_) {
scope = $rootScope.$new();
controller = $controller('mainController', {
$scope: scope
});
mainService = _mainService_;
}));
it('rereshes data', function() {
var pattern = 'abcde';
scope.refreshData(pattern);
expect(mainService.getData).toHaveBeenCalledWith(pattern);
});
})
<link href="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine.css" rel="stylesheet" />
<script src="//safjanowski.github.io/jasmine-jsfiddle-pack/pack/jasmine-2.0.3-concated.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-mocks.js"></script>

Exchanging module values in AngularJS test

I have a module with a greet factory attached to it:
angular.module('someModule', [])
.factory('greet', function(name) {
return function() {
return 'Hi ' + name + '!';
}
});
This factory injects a name which is a value defined in some other module.
angular.module('someOtherModule', [])
.value('name', 'example');
When testing this module, I would like to be able to change the value of my injectable name multiple times (once for each test) so that my tests can look something like:
// In my test file…
// Initialise the module I am testing `greet` upon, and mock the other module which has a `name` value
beforeEach(mocks.module('someModule', function ($provider) {
$provider.value('name', 'Bob');
}));
var greet
beforeEach(mocks.inject(function ($injector) {
greet = $injector.get('greet');
});
it('should say "Bob"', function () {
expect(greet()).toBe('Hi Bob!');
});
// Now I need to change the `name` value to be "Bar" instead
it('should say "Bar"', function () {
expect(greet()).toBe('Hi Bar!');
});
How is this possible?
The two modules are composed with my app module:
angular.module('app', ['someModule', 'someOtherModule'])
You can use $provide.value('name', 'Bob'); to inject the value.
var myApp = angular.module('myApp', []);
myApp.factory('greet', function (name) {
return function () {
return 'Hi ' + name + '!';
}
});
myApp.value('name', 'example');
describe('myApp', function () {
beforeEach(angular.mock.module('myApp'));
it('should say "Bob"', function () {
module(function ($provide) {
$provide.value('name', 'Bob');
});
angular.mock.inject(function ($injector) {
var greet = $injector.get('greet');
expect(greet()).toBe('Hi Bob!');
})
});
it('should say "Bar"', function () {
module(function ($provide) {
$provide.value('name', 'Bar');
});
angular.mock.inject(function ($injector) {
var greet = $injector.get('greet');
expect(greet()).toBe('Hi Bar!');
})
});
});
I created a demo for you and hope it can shed some light!
Updated: demo
Demo
I was able to get this working, but I had to make a slight change to how the name dependency was injected (pointed out in the comment below). Given the app code:
angular.module("app", ["some", "other"]);
angular.module("some", []).factory('greet', function(name) { // name injected here
return function() { // the name dependency moved from here up one function signature
return 'Hi ' + name + '!';
};
});
angular.module("other", []).value('name', 'example');
This is as DRY as I could make the test:
describe("greet", function() {
var provide = injector = greet = undefined;
beforeEach(function() {
module('app', function($provide) {
provide = $provide;
});
inject(function($injector) {
injector = $injector;
});
greet = function() {
return injector.get('greet')();
};
});
describe("#greet", function() {
it("says Hi Bob", function() {
provide.value('name', 'Bob');
expect(greet()).toBe('Hi Bob!');
});
it("says Hi Biff", function() {
provide.value('name', 'Biff');
expect(greet()).toBe('Hi Biff!');
});
});
});

Categories

Resources