Say I have a service like this, where a car gets an engine service injected, which is a constructor function:
angular.module('car', ['engine']).factory('carCreator', function( engine ) {
var carCreator = function( settings ) {
var engineInstance = engine( settings );
engineInstance.setMiles( settings.engine.miles );
return {
brand: settings.brand;
engine: engineInstance;
}
};
return carCreator;
});
How do I test both lines in the initialization logic:
var engineInstance = engine( settings );
engineInstance.setMiles( settings.engine.miles )
1: That engine is called with settings
2: That engineInstance.setMiles is called with settings.engine.miles
This is what I'm doing right now, but with no luck:
describe('initialization', function() {
var carCreator;
var settings = {
brand: 'Ford',
engine: {
miles: 12000
}
};
var mockEngineInstance = {
setMiles: function() {}
};
window.mockEngineCreator = function() {
return mockEngineInstance;
}
beforeEach(module('car', function($provide) {
$provide.value('engine', mockEngineCreator );
}));
beforeEach(inject(function(_carCreator_) {
carCreator = _carCreator_;
}));
it('should init text object correctly on initialization', function() {
spyOn(window, 'monkEngineCreator');
spyOn(mockEngineInstance, 'setMiles');
carCreator( settings );
expect(window.mockEngineCreator).toHaveBeenCalledWith( settings );
expect(mockEngineInstance.setMiles).toHaveBeenCalledWith( settings.engine.miles );
});
});
but this test fails, saying that window.mockEngineCreator never was called. It seems that $provide creates a new copy of the function passed in, instead of keeping a reference to it. So, does anyone know how to setup a test that can test this correctly?
The reason for this is that when you run spyOn(window, 'monkEngineCreator'), the spy is put on window, not the injected value that angular uses for dependency injection.
Doing something like this should work: (untested code)
beforeEach(module('car', function($provide){
$provide.value('engine', jasmine.createSpy('engineSpy').andCallFake(function(){
return mockEngineInstance;
}));
}));
it('should init', inject(function(carCreator, engine){
var settings = {};
carCreator(settings);
expect(engine).toHaveBeenCalledWith(settings);
}));
Related
With the help of user3696882 I managed to set up a service:
app.service('metro', function() {
this.flagValue = {value: false};
this.setFlagValue = function(flagValue){
this.flagValue.value = flagValue;
};
this.getFlagValue = function(){
return this.flagValue;
};
});
that forms the model for a boolean that is being toggled true/false every second by one of my controllers:
function GlobalCtrl( $scope , $interval , metro ) {
var th = this;
$interval(function () {
th.pulse = !th.pulse;
metro.setFlagValue( th.pulse );
}, 1000 );
}
The toggling model in my service can show up in another part of the view - accessed by a different controller:
function ViewCtrl( $scope , metro ) {
var th = this;
th.pulse = metro.getFlagValue() ;
}
Full solution available in this Plunker
Maybe I am taking things too far, but I'd like to be able to have a service depending on $interval which toggles its own model.
I made:
app.factory('loop_toggle', ['$interval', function($interval ) {
var flagObj = {value: false};
function flip() {
flagObj.value = !flagObj.value;
}
$interval(flip, 1000);
return {
current : flagObj
};
}]);
However, with my modifed controller:
function ViewCtrl( $scope , metro , loop_toggle ) {
var th = this;
th.pulse = metro.getFlagValue() ;
th.loop = loop_toggle.current.value;
}
I cannot see anything toggling: just a static "false".
Development so far available in this Plunker
I suppose I could make do with a service metronome being toggled by a controller - but isn't that having business logic in the wrong place?
You need to update th.loop after each second,
Add dependaincies into ViewCtrl
ViewCtrl.$inject = ['$scope' , 'metro' , 'loop_toggle', '$interval' ] ;
Update your th.loop using $interval
function ViewCtrl( $scope , metro , loop_toggle, $interval ) {
var th = this;
th.pulse = metro.getFlagValue() ;
$interval(function () {
th.loop = loop_toggle.current.value;
}, 1000 );
}
Updated plunker
metro service doesn't need $interval, but Why loop_toggle factory needs $interval to update value?
Here are some explanations Service vs Factory
I have a project that uses angular's $http service to load data from a remote location. I want to use rxjs Observables so the call in my service looks like this:
userInfo() : Rx.Observable<IUserInfo> {
var url : string = someUrl + this._accessToken;
return Rx.Observable.fromPromise<IUserInfo>( this.$http.get<IUserInfo>( url ) );
}
and this is subscribed to by my controller like this:
getUserInfo() : void {
this._googleService.userInfo().subscribe(
( result ) => { this.handleUserInfo( result ) },
( fault : string ) => this.handleError( fault )
)
}
private handleUserInfo( result : IHttpPromiseCallbackArg<IUserInfo> ) : void {
console.log( "User info received at " + new Date() );
this._name = result.data.given_name + " " + result.data.family_name;
this._email = result.data.email;
this._profilePicUrl = result.data.picture;
}
the problem is that despite the name, email and profile pic being updated these changes are not visible. As soon as anything else triggers an angular $apply the changes appear but because of the Observable these changes in the controller happen after the angular digest loop that is triggered by the $http call.
This does work correctly if my service just returns a promise to the controller.
How do I update my view in this case? I do not want to manually have to wire up each observable to trigger a digest cycle. I want all Observables to trigger a digest cycle when they receive a new value or error.
We can use the ScopeScheduler from rx.angular.js for this. We only have to create a new one where we create our angular module and pass the $rootScope to it:
const module : ng.IModule = angular.module( 'moduleName', [] );
module.run( ["$rootScope", ( $rootScope ) => {
new Rx.ScopeScheduler( $rootScope );
}]);
That's all you have to do. Now all Rx.Observables trigger an $apply when they get a new value.
For some reason the ScopeScheduler was deleted when the rx.angular.js library was upgraded to rxjs version 4. We have to use rx.angular.js version 0.0.14 to use the ScopeScheduler.
I do not know what the suggested solution to this is in version 4.
A project using this fix can be viewed here:
https://github.com/Roaders/Typescript-OAuth-SPA/tree/observable_apply_issues
I couldn't get the Rx.ScopeScheduler method to work, so I just overwrote the rx observable subscribe method itself instead, and wrapped the callbacks in $rootScope.$apply :)
module.run(['$rootScope', 'rx', function ($rootScope, rx) {
rx.Observable.prototype.subscribe = function (n, e, c) {
if(typeof n === 'object') {
return this._subscribe(n);
}
var onNext = function(){};
if(n) {
onNext = function(value) {
if($rootScope.$$phase) {
n(value);
}
else {
$rootScope.$apply(function(){ n(value); });
}
};
}
var onError = function(err) { throw err; };
if(e) {
onError = function(error) {
if($rootScope.$$phase) {
e(error);
}
else {
$rootScope.$apply(function(){ e(error); });
}
};
}
var onCompleted = function(){};
if(c) {
onCompleted = function() {
if($rootScope.$$phase) {
c();
}
else {
$rootScope.$apply(function(){ c(); });
}
};
}
return this._subscribe(
new rx.AnonymousObserver(onNext, onError, onCompleted)
);
};
}]);
I'm in the process of transferring all of our code onto Karma and Jasmine and am having a hard time figuring out where I start.
What would this code look like had I started building it from a TDD standpoint? What does a simple test look like?
Note: This code works 100%, but I don't have any tests setup.
(function() {
"use strict";
angular.module('system_centers', [
'system'
])
.factory('System', ['Api', function(Api) {
this.loadSystem = function(contactId, cardId) {
return Api.get('lmc/contact/system/' + contactId, {
card_id: cardId
});
};
this.completeSystem = function(recordId) {
return Api.put('system/complete/' + recordId);
};
this.createSystem = function(contactId, cardId) {
if (+contactId === 0 || +cardId === 0) {
return false;
}
return Api.post('contact/system/' + contactId, {
card_id: cardId,
type: 'systems',
origin: 'lmc'
});
};
return this;
}])
.controller('System_centersCtrl', ['$scope', 'System', function($scope, System) {
$scope.main.cardType = 'systems';
$scope.main.type = 'system_centers';
$scope.completeSystem = function(recordId) {
System.completeSystem(recordId).success(function(){
toastr.success("System completed!");
$scope.createSystem();
$scope.loadSystems();
});
};
$scope.createSystem = function() {
System.createSystem($scope.main.contactId, $scope.main.cardId).success(function() {
$scope.loadSystem($scope.main.contactId, $scope.main.cardId);
$scope.loadContacts();
});
};
$scope.loadSystem = function() {
System.loadSystem($scope.main.contactId, $scope.main.cardId).success(function(data) {
if (data.error) {
$scope.createSystem();
} else {
$scope.main.record = data.record;
}
});
};
$scope.loadSystems();
}]);
})();
Testing is easy, you just need to assert that your factory is working correctly. This doesn't mean that you want actually get/put/post stuff, that belongs to the Api test. Here we just want to know that calling certain functions of our factory will call some Api functions with the correct parameters.
I imagine that Api belongs to the system module. I load it and mock it:
beforeEach(module('system', function($provide) {
api = {
get: function(url, params) {},
put: function(url, params) {},
post: function(url, params) {}
};
spyOn(api, 'get');
spyOn(api, 'put');
spyOn(api, 'post');
$provide.value('Api', api);
}));
module will load your system module and then we just need to create a simple object with the interface of our Api service. No need to implement anything on them.
Then we just need to spy the methods (to be able to assert that they have been called).
Next, we load the system_centers module and we inject our services:
beforeEach(module('system_centers'));
beforeEach(inject(function(System) {
system = System;
}));
inject is used to inject dependencies in our tests. We just need to inject our System factory.
What rest are the test, I created a bunch of them:
it('should load the system', function() {
system.loadSystem(1, 0);
expect(api.get).toHaveBeenCalledWith('lmc/contact/system/1', {card_id : 0});
});
it('should be able to complete the system', function() {
system.completeSystem(20);
expect(api.put).toHaveBeenCalledWith('system/complete/20');
});
it('should create the system', function() {
system.createSystem(1, 3);
expect(api.post).toHaveBeenCalledWith('contact/system/1', { card_id: 3, type: 'systems', origin: 'lmc'});
});
it('should not create the system if contact_id is 0', function() {
system.createSystem(0, 20);
expect(api.post).not.toHaveBeenCalled();
});
it('should not create the system if card_id is 0', function() {
system.createSystem(1, 0);
expect(api.post).not.toHaveBeenCalled();
});
They are much the same. We call some factory method and we expect that our Api has been called with some parameters. Or even that calling createSystem with contact or card id with 0 won't call the Api.
Well, this is a good head start. You can continue with more tests or with other parts of your application.
Here is the plunker: http://plnkr.co/edit/5vfg0Y1G0vo2nnz0xByN?p=preview
I am trying to write unit tests for my Angular.js application but I cannot manage to inject what I need (it is not able to find a suitable provider).
Does anyone see what I missed?
Firefox 21.0 (Linux) filter staticList should convert static list object into its display value FAILED
Error: Unknown provider: staticListProvider <- staticList in /path/to/my-app/public/third-party/angular/angular.js (line 2734)
createInjector/providerInjector<#/path/to/my-app/public/third-party/angular/angular.js:2734
getService#/path/to/my-app/public/third-party/angular/angular.js:2862
createInjector/instanceCache.$injector<#/path/to/my-app/public/third-party/angular/angular.js:2739
getService#/path/to/my-app/public/third-party/angular/angular.js:2862
invoke#/path/to/my-app/public/third-party/angular/angular.js:2880
workFn#/path/to/my-app/test/lib/angular/angular-mocks.js:1778
angular.mock.inject#/path/to/my-app/test/lib/angular/angular-mocks.js:1764
#/path/to/my-app/test/unit/filtersSpec.js:19
#/path/to/my-app/test/unit/filtersSpec.js:16
#/path/to/my-app/test/unit/filtersSpec.js:3
The application:
angular.module('myApp', ['myAppFilters', 'ui.bootstrap', '$strap.directives']).
// Some other stuff
The filters:
"use strict";
angular.module('myAppFilters', []).
filter('staticList', function () {
return function (listItem) {
if (!listItem) {
return '';
}
return listItem.value;
};
});
The test:
'use strict';
describe('filter', function () {
beforeEach(angular.module('myAppFilters'));
describe('staticList', function () {
it('should convert static list object into its display value',
inject(function (staticList) {
expect(undefined).toBe('');
expect({key: 'A', value: 'B'}).toBe('B');
}));
});
});
The Karma configuration:
basePath = '../';
files = [
JASMINE,
JASMINE_ADAPTER,
'public/third-party/jquery/*.js',
'public/third-party/angular/angular.js',
'public/third-party/angular/i18n/angular-*.js',
'public/third-party/moment/moment.min.js',
'public/third-party/moment/moment-*.js',
'public/js/**/*.js',
'test/lib/**/*.js',
'test/unit/**/*.js'
];
colors = true;
autoWatch = true;
browsers = ['Firefox'];
junitReporter = {
outputFile: 'test_out/unit.xml',
suite: 'unit'
};
If anybody wants to see the full code, the application repository is here: https://github.com/adericbourg/GestionCourrier
Thanks a lot,
Alban
In your inject code
it('should convert static list object into its display value',
inject(function (staticList) {
expect(undefined).toBe('');
expect({key: 'A', value: 'B'}).toBe('B');
}));
replace "inject(function (staticList)" with "inject(function (staticListFilter)". This is some random convention angular is following. You can check comments on this page for more info http://docs.angularjs.org/tutorial/step_09
I faced similar problem, but in my case my filter name contained suffix 'Filter' which caused the same injection problem.
.filter('assetLabelFilter', function(){
return function(assets, selectedLabels){
// Implementation here
};
});
I was finally able to solve the problem by manually injecting the filter to my test
'use strict';
describe('assetLabelFilter', function() {
beforeEach(module('filters.labels'));
var asset1 = {labels: ['A']};
var asset2 = {labels: ['B']};
var asset3 = {labels: []};
var asset4 = {labels: ['A', 'B']};
var assets = [asset1, asset2, asset3, asset4];
var assetLabelFilter;
beforeEach(inject(function($filter) {
assetLabelFilter = $filter('assetLabelFilter');
}));
it('should return only assets with selected label', function() {
var selectedLabels = ['B'];
expect(assetLabelFilter(assets, selectedLabels)).toEqual([asset2, asset4]);
});
});
The nice answer above made me realize that in order to use the angular tutorial way:
it('should ', inject(function(assetLabelFilter) {
var selectedLabels = ['B'];
expect(assetLabelFilter(assets, selectedLabels)).toEqual([asset2, asset4]);
}));
The filter name can't have the suffix 'Filter' because it is magic word as mentioned in the answer above.
To demonstrate the scenario I also created a Plunker
I'm trying to get the following findTimelineEntries function inside an Angular controller executing after saveInterview finishes:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId}, function() {
$scope.findTimelineEntries();
});
};
The save action adds or edits data that also is part of the timeline entries and therefore I want the updated timeline entries to be shown.
First I tried changing it to this:
$scope.saveInterview = function() {
var functionReturned = $scope.interviewForm.$save({employeeId: $scope.employeeId});
if (functionReturned) {
$scope.findTimelineEntries();
}
};
Later to this:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId});
};
$scope.saveInterview.done(function(result) {
$scope.findTimelineEntries();
});
And finaly I found some info about promises so I tried this:
$scope.saveInterview = function() {
$scope.interviewForm.$save({employeeId: $scope.employeeId});
};
var promise = $scope.saveInterview();
promise.done(function() {
$scope.findTimelineEntries();
});
But somehow the fact that it does work this way according to http://nurkiewicz.blogspot.nl/2013/03/promises-and-deferred-objects-in-jquery.html, doesn't mean that I can use the same method on those $scope.someFuntcion = function() functions :-S
Here is a sample using promises. First you'll need to include $q to your controller.
$scope.saveInterview = function() {
var d = $q.defer();
// do something that probably has a callback.
$scope.interviewForm.$save({employeeId: $scope.employeeId}).then(function(data) {
d.resolve(data); // assuming data is something you want to return. It could be true or anything you want.
});
return d.promise;
}