I've been looking into unit testing a project I'm doing for Angular (my first). I've set up a DataTree service that depends on a NodeFactory and also makes $http calls.
First off, (a snippet of) my test:
describe("the data tree service", function() {
let DataTree, NodeFactory, $httpBackend;
beforeEach(module('memApp.common'));
beforeEach( inject(function(_DataTree_, _NodeFactory_, _$httpBackend_) {
DataTree = _DataTree_;
NodeFactory = _NodeFactory_;
$httpBackend = _$httpBackend_;
}));
describe("is sane, so it", function() {
$httpBackend
.when('GET', 'json/home.json')
.respond(200, {
"esv": {
"_title" : "ESV",
"_filePath" : "json/bookData.json"
}
});
...
}
...
}
It is trying to set up a mock backend responding with JSON.
My Karma tests fail on the $httpBackend.when, specifically:
TypeError: Cannot read property 'when' of undefined
at <the httpBackend.when call>
My DataTree service (a service service - just want one global instance of it):
(function() {
angular.module('memApp.common')
.service('DataTree', ['NodeFactory', '$http', '$q',
function(NodeFactory, $http, $q) {
...
}]);
})();
My NodeFactory factory - a wrapper for a class:
(function() {
angular.module('memApp.common')
.factory('NodeFactory', function() {
....
});
})();
Finally, my included karma.conf.js files:
files: [
'bower_components/angular/angular.min.js',
'bower_components/angular-mocks/angular-mocks.js',
'bower_components/angular-route/angular-route.js',
'js/common/common.module.js', //memApp.common module def'd here
'js/common/node.service.js', //NodeFactory def'd here
'js/common/datatree.service.js', //DataTree def'd here
'js/common/common.controller.js',
'js/hierarchy/hierarchy.module.js',
'js/hierarchy/hierarchy.controller.js',
'js/rehearsal/rehearsal.module.js',
'js/rehearsal/rehearsal.controller.js',
'js/app/app.module.js',
'js/hierarchy/hierarchy.directive.js',
'js/rehearsal/rehearsal.directive.js',
'spec/*Spec.js' //all Jasmine tests here - in particular, there is a test suite for NodeFactory here.
],
I don't think this is relevant, but NodeFactory's test does the following (and I have been burned by what other tests are doing before...):
describe("the Node service", function() {
let NodeFactory;
let home;
beforeEach(module('memApp.common'));
beforeEach( inject(function(_NodeFactory_) {
NodeFactory = _NodeFactory_;
home = NodeFactory.create();
}));
...
});
I have gotten tests in my data tree service to pass without doing anything with $httpBackend. This sorta worries me as I think my service shouldn't be making actual $http calls in a unit test?
I am a bit of a newbie to ng-mock (and am taking the PluralSight course on said topic) but I'm trying to follow along with my project and this is blocking me from doing much. In general, $httpBackend still feels a tad magical.
Well, heh...writing out my question sometimes goes a long way in answering it. (I spent a day looking at this, I swear.)
My call to $httpBackend wasn't contained in a test (just a describe block), so the beforeEach wasn't being called before it. I moved the code into an it...test, and the injection now works.
Related
Is it possible to spy on a service in a karma test that was wired by Angular?
Example: myService is the unit under test. thirdParty stands for a third party service that should be spied on.
.service('thirdParty', function() {
return {
hello: function() {
return 'hello world';
}
}
})
.service('myService', function(thirdParty) {
return {
world: function() {
return thirdParty.hello();
}
}
})
In my karma test I would like to spy on thirdParty service and call the real service:
describe('spy', function() {
var thirdParty, myService;
beforeEach(inject(function(_thirdParty_, _myService_) {
myService = _myService_;
thirdParty = _thirdParty_;
spyOn(thirdParty, 'hello').andCallThrough();
}));
it('should be called in myService', function() {
expect(thirdParty.hello).toHaveBeenCalled();
expect(myService.world()).toBe('hello world');
});
})
The point is that my test should assert that
a specific method of the third party service has been called inside myService
the third party service doesn't change its internal behaviour that would lead to a an exception or unexpected result (e.g. after a library update)
The myService.world() assertion just works but as I expect myService doesn't operate on the spied thirdParty service.
The result is:
Expected spy hello to have been called.
In some tests I'm already mocking third party services with a provider and a bare mock.
So I tried to create a spying instance of cacheFactory that comes with angular-cache:
beforeEach(module('angular-cache'));
beforeEach(module(function($provide, $injector, CacheFactoryProvider) {
//CacheFactoryProvider requires $q service
var q = $injector.get('$q');
var cacheFactory = CacheFactoryProvider.$get[1](q);
spyOn(cacheFactory, 'createCache').andCallThrough();
$provide.factory('CacheFactory', cacheFactory);
}));
Now I`m facing the chicken-and-egg problem:
Error: [$injector:modulerr] Failed to instantiate module function ($provide, $injector, CacheFactoryProvider) due to:
Error: [$injector:unpr] Unknown provider: $q
I know that this example can't work but because of lack of knowledge of the internals how Angular is actually instantiating and wiring services I would like to ask the community whether my test approach is possible or even sane. Thanks for help.
Instead of
it('should be called in myService', function() {
expect(thirdParty.hello).toHaveBeenCalled();
expect(myService.world()).toBe('hello world');
});
the test should be
it('should be called in myService', function() {
expect(myService.world()).toBe('hello world');
expect(thirdParty.hello).toHaveBeenCalled();
});
Indeed, the thirdParty.hello method won't have been called until you actually call myService.world().
While it is fairly easy to unit test services/controllers in angular it seems very tricky to test decorators.
Here is a basic scenario and an approach I am trying but failing to get any results:
I defined a separate module (used in the main app), that is decorating $log service.
(function() {
'use strict';
angular
.module('SpecialLogger', []);
angular
.module('SpecialLogger')
.config(configureLogger);
configureLogger.$inject = ['$provide'];
function configureLogger($provide) {
$provide.decorator('$log', logDecorator);
logDecorator.$inject = ['$delegate'];
function logDecorator($delegate) {
var errorFn = $delegate.error;
$delegate.error = function(e) {
/*global UglyGlobalFunction: true*/
UglyGlobalFunction.notify(e);
errorFn.apply(null, arguments);
};
return $delegate;
}
}
}());
Now comes a testing time and I am having a really hard time getting it working. Here is what I have come up with so far:
(function() {
describe('SpecialLogger module', function() {
var loggerModule,
mockLog;
beforeEach(function() {
UglyGlobalFunction = jasmine.createSpyObj('UglyGlobalFunctionMock', ['notify']);
mockLog = jasmine.createSpyObj('mockLog', ['error']);
});
beforeEach(function() {
loggerModule = angular.module('SpecialLogger');
module(function($provide){
$provide.value('$log', mockLog);
});
});
it('should initialize the logger module', function() {
expect(loggerModule).toBeDefined();
});
it('should monkey patch native logger with additional UglyGlobalFunction call', function() {
mockLog.error('test error');
expect(mockLog.error).toHaveBeenCalledWith('test error');
expect(UglyGlobalFunction.notify).toHaveBeenCalledWith('test error');
});
});
}());
After debugging for a while I have noticed that SpecialLogger config section is not even fired.. Any suggestions on how to properly test this kind of scenario?
You're missing the module('SpecialLogger'); call in your beforeEach function.
You shouldn't need this part: loggerModule = angular.module('JGM.Logger');
Just include the module and inject the $log. Then check if your decorator function exists and behaves as expected.
After some digging I came up with a solution. I had to create and inject my own mocked $log instance and only then I was able to check weather or not calling error function also triggers call to the global function I was decorating $log with.
The details can be found on a blog post I wrote to explain this problem in detail. Plus I open sourced an anuglar module that makes use of this functionality available here
I am new to AngularJS and JasmineJS.
I have decorated the Angular $log service to do logging over http. I verified that it works fine in the program itself. However, when running the Jasmine unit tests, the $log calls create errors. I get messages such as the following:
TypeError: TypeError: $log.warn.logs is undefined in
http...[snip]...angular-mocks.js (line 316)
The messages follow this pattern for all the different $log functions ($log.error.logs, $log.info.logs, etc.).
My current injector looks like this:
var $httpBackend, log;
beforeEach(inject(function ($injector, $log) {
log = $log;
//lots of mock http service things here
}));
I have also tried this:
beforeEach(inject(['$log', function (log) {
$log = log;
}]));
And this:
beforeEach(inject(function ($injector) {
log = $injector.get('$log');
// mock http things here
}));
A failing test looks like this:
it('should log error', function () {
$httpBackend.expectPOST('the url for logging');
var controller = createController();
log.warn('Test warning');
$httpBackend.flush();
});
All calls to $log in the code being tested result in the same error. When I commented them out and removed the failing test above, all unit tests passed.
How should I set up the unit tests correctly?
The answer is that there is something wrong with angular-mocks.js and it is not instantiating the log arrays correctly. I downloaded the latest non-beta copy, and it's still doing the same thing. The instantiation of arrays happens in their code using a call to $log.reset(), but somehow, that part of their code is not being reached. Therefore, I fixed my problem by manually calling $log.reset():
//This works
var $log;
beforeEach(inject(function ($injector) {
$log = $injector.get('$log');
$log.reset();
//continue other stuff
}));
//This also works
var log;
beforeEach(inject(function ($injector, $log) {
log = $log;
log.reset();
//continue other stuff
}));
I am writing unit tests for a controller. This controller has a $resource service injected :
function controller($scope, Service) {
Service.get(function(result){
// do stuff with the result, not relevant here
}
}
The service is defined like this :
angular.module('so').factory('Service', ['$resource', service]);
function service($resource) {
return $resource('/url', null, {
get: { method: 'POST', params: {}, isArray: false}
});
}
My Jasmine unit test is the following :
describe("Controller", function(){
var $httpBackend;
beforeEach(function() {
module('so');
inject(function( _$httpBackend_) {
$httpBackend = _$httpBackend_;
});
});
it('should have done stuff irrelevant to the question', function() {
var $injector = angular.injector('so'),
$scope = $injector.get('$rootScope'),
$httpBackend
.whenPOST('/url')
.respond ([]);
// controller needs to be defined here and not in the beforeEach as there
// are more parameters passed to it, depending on the test
var controller = $injector.get('$controller')('controller', { "$scope": $scope });
$httpBackend.flush();
// then here the actual test resolution, also irrelevant
});
});
I get an error when running the test :
Error: No pending request to flush ! in file:///path/to/angular-mock.js (line 1453)
I added a console.log() in the callback from Service.get() and indeed, it is not called (everything outside of the callback is of course called). Also tried to add a scope digest if not phased after controller creation in the unit test, as I saw suggested in an other question, with no luck.
I know that I can mock that in some other ways, but using $httpBackend seems the perfect solution for the test : mocking the webserver and the data received.
I'm using AngularJS 1.2.16 (can't upgrade to 1.3.*, IE 8 compatibility required). I first used 1.2.13 and updated to check if it would solve the issue, without any luck.
That was an injection issue that was solved by changing the test from
it('should have done stuff irrelevant to the question', function() {
var $injector = angular.injector('so'),
$scope = $injector.get('$rootScope'),
$httpBackend
.whenPOST('/url')
.respond ([]);
// controller needs to be defined here and not in the beforeEach as there
// are more parameters passed to it, depending on the test
var controller = $injector.get('$controller')('controller', { "$scope": $scope });
$httpBackend.flush();
// then here the actual test resolution, also irrelevant
});
To:
it('should have done stuff irrelevant to the question', inject(function(Service) {
// edited lines because they did not change
var controller = $injector.get('$controller')('controller', { "$scope": $scope, "Service": Service });
// edited lines because they did not change
}));
So basicaly, adding the inject() in the test function and passing the service to the controller "manually".
I found the issue, that's great, but I don't really understand why it doesn't work. Also, I tried this right after finding the solution :
it('should have done stuff irrelevant to the question', inject(function() {
// edited lines because they did not change
var Service = $injector.get('Service'),
var controller = $injector.get('$controller')('controller', { "$scope": $scope, "Service": Service });
// edited lines because they did not change
}));
but this fail again, with the same "no pending request" error. I'm guessing that's some sort of racing issue, where my service can't get the proper $httpBackend to be injected when it's created afterwards, but I don't really understand why this occurs. If anybody can enlighten me... I'll be grateful.
Does anyone have an idea how to mock $httpBackend in angular e2e tests?
The idea is stubbing XHR requests while running tests on travis-ci.
I'm using karma to proxy assets and partials from my rails app running on travis.
I want to do acceptance testing without real DB queries.
Here is part of my karma config file:
...
files = [
MOCHA,
MOCHA_ADAPTER,
'spec/javascripts/support/angular-scenario.js',
ANGULAR_SCENARIO_ADAPTER,
'spec/javascripts/support/angular-mocks.js',
'spec/javascripts/e2e/**/*_spec.*'
];
...
proxies = {
'/app': 'http://localhost:3000/',
'/assets': 'http://localhost:3000/assets/'
};
...
Here is part of my spec file:
beforeEach(inject(function($injector){
browser().navigateTo('/app');
}));
it('should do smth', inject(function($rootScope, $injector){
input('<model name>').enter('smth');
//this is the point where I want to stub real http query
pause();
}));
I have tried to receive $httpBackend service through $injector:
$injector.get('$httpBackend')
But this is not the one that is used inside iframe where my tests run.
The next try I made was using angular.scenario.dsl, here is code samle:
angular.scenario.dsl('mockHttpGet', function(){
return function(path, fakeResponse){
return this.addFutureAction("Mocking response", function($window, $document, done) {
// I have access to window and document instances
// from iframe where my tests run here
var $httpBackend = $document.injector().get(['$httpBackend']);
$httpBackend.expectGET(path).respond(fakeResponse)
done(null);
});
};
});
Usage example:
it('should do smth', inject(function($rootScope, $injector){
mockHttpGet('<path>', { /* fake data */ });
input('search.name').enter('mow');
pause();
}));
This leads to following error:
<$httpBackend listing> has no method 'expectGET'
So, at this point I have no idea of next step. Have anyone tried doing something like this, is this type of stubbing really possible?
If you are really trying to mock out the backend in a E2E test (these tests are called Scenarios, while Specs are used for unit testing) then this is what I did in a project I was working on earlier.
The application I was testing was called studentsApp. It was an application to search for students by querying a REST api. I wanted to test the application without actually querying that api.
I created a E2E application called studentsAppDev that I inject studentsApp and ngMockE2E into. There I define what calls the mockBackend should expect and what data to return. The following is an example of my studentsAppDev file:
"use strict";
// This application is to mock out the backend.
var studentsAppDev = angular.module('studentsAppDev', ['studentsApp', 'ngMockE2E']);
studentsAppDev.run(function ($httpBackend) {
// Allow all calls not to the API to pass through normally
$httpBackend.whenGET('students/index.html').passThrough();
var baseApiUrl = 'http://localhost:19357/api/v1/';
var axelStudent = {
Education: [{...}],
Person: {...}
};
var femaleStudent = {
Education: [{...}],
Person: {...}
};
$httpBackend.whenGET(baseApiUrl + 'students/?searchString=axe&')
.respond([axelStudent, femaleStudent]);
$httpBackend.whenGET(baseApiUrl + 'students/?searchString=axel&')
.respond([axelStudent, femaleStudent]);
$httpBackend.whenGET(baseApiUrl + 'students/?searchString=axe&department=1&')
.respond([axelStudent]);
$httpBackend.whenGET(baseApiUrl + 'students/?searchString=axe&department=2&')
.respond([femaleStudent]);
$httpBackend.whenGET(baseApiUrl + 'students/?searchString=axe&department=3&')
.respond([]);
...
$httpBackend.whenGET(baseApiUrl + 'departments/?teachingOnly=true')
.respond([...]);
$httpBackend.whenGET(baseApiUrl + 'majors?organization=RU').respond([...]);
});
Then, I have a first step in my Jenkins CI server to replace the studentsApp with studentsAppDev and add a reference to angular-mocks.js in the main index.html file.
Mocking out your backend is an important step in building a complex Angular application. It allows testing to be done without access to the backend, you don't test things twice and there are less dependencies to worry about.
Angular Multimocks is a simple way to test how your app behaves with different responses from an API.
It allows you to define sets of mock API responses for different scenarios as JSON files.
It also allows you to change scenarios easily. It does this by allowing
you to compose “scenarios” out of different mock files.
How to add it to your app
After adding the required files into your page, simply add scenario as a dependency to your application:
angular
.module('yourAppNameHere', ['scenario'])
// Your existing code here...
Once you have added this to your app you can start to create mocks for API calls.
Lets say your app makes the following API call:
$http.get('/games').then(function (response) {
$scope.games = response.data.games;
});
You can create a default mock file:
Example of someGames.json
{
"httpMethod": "GET",
"statusCode": 200,
"uri": "/games",
"response": {
"games": [{"name": "Legend of Zelda"}]
}
}
When you load your application, calls to /games will return 200 and {"games": [{"name": "Legend of Zelda"}]}
Now lets say you want to return a different response for the same API call, you can place the application in a different scenario by changing the URL e.g. ?scenario=no-games
The no-games scenario can use a different mock file, lets say one like this:
Example of noGames.json
{
"httpMethod": "GET",
"statusCode": 200,
"uri": "/games",
"response": {
"games": []
}
}
Now when you load your application, calls to /games will return 200 and {"games": []}
Scenarios are composed of various JSON mocks in a manifest like this:
{
"_default": [
"games/someGames.json"
],
"no-games": [
"games/noGames.json"
]
}
You can then exclude the mock files and strip the scenario dependency in your production app.
This feels more like unit/spec testing. Generally speaking you should use mocks within unit/spec tests rather than e2e/integration tests. Basically, think of e2e tests as asserting expectations on a mostly integrated app...mocking out things kind of defeats the purpose of e2e testing. In fact, I'm not sure how karam would insert angular-mocks.js into the running app.
The spec test could look something like...
describe('Controller: MainCtrl', function () {
'use strict';
beforeEach(module('App.main-ctrl'));
var MainCtrl,
scope,
$httpBackend;
beforeEach(inject(function ($controller, $rootScope, $injector) {
$httpBackend = $injector.get('$httpBackend');
$httpBackend.when('GET', '/search/mow').respond([
{}
]);
scope = $rootScope.$new();
MainCtrl = $controller('MainCtrl', {
$scope: scope
});
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should search for mow', function () {
scope.search = 'mow';
$httpBackend.flush();
expect(scope.accounts.length).toBe(1);
});
});