I have the following angular controller
function IndexCtrl($scope, $http, $cookies) {
//get list of resources
$http.get(wtm.apiServer + '/v1/developers/me?access_token=' + $cookies['wtmdevsid']).
success(function(data, status, headers, config) {
// snip
}).
error(function(data, status, headers, config) {
// snip
});
$scope.modal = function() {
// snip
}
return;
}
What I am trying to do is mock the get method on the $http service. Here's my unit test code:
describe('A first test suite', function(){
it("A trivial test", function() {
expect(true).toBe(true);
});
});
describe('Apps', function(){
describe('IndexCtrl', function(){
var scope, ctrl, $httpBackend;
var scope, http, cookies = {wtmdevsid:0};
beforeEach(inject(function($injector, $rootScope, $controller, $http) {
scope = $rootScope.$new();
ctrl = new $controller('IndexCtrl', {$scope: scope, $http: $http, $cookies: cookies});
spyOn($http, 'get');
spyOn(scope, 'modal');
}));
it('should create IndexCtrl', function() {
var quux = scope.modal();
expect(scope.modal).toHaveBeenCalled();
expect($http.get).toHaveBeenCalled();
});
});
});
When I run this I get
ReferenceError: wtm is not defined.
wtm is a global object and of course it wouldn't be defined when I run my test because the code that it is declared in is not run when I run my test. What I want to know is why the real $http.get function is being called and how do I set up a spy or a stub so that I don't actually call the real function?
(inb4 hating on globals: one of my coworkers has been tasked with factoring those out of our code :) )
You need to wire up the whenGET method of your $httpBackend in advance of your test. Try setting it up in the beforeEach() function of your test... There is a good example here under "Unit Testing with Mock Backend".
I suggest all globals used the way you described here should be used through the $window service.
All global variables that are available, such as as window.wtm, will also be available on $window.atm.
Then you can stub out your wtm reference completely and spy on it the same way you already described:
var element, $window, $rootScope, $compile;
beforeEach(function() {
module('fooApp', function($provide) {
$provide.decorator('$window', function($delegate) {
$delegate.wtm = jasmine.createSpy();
return $delegate;
});
});
inject(function(_$rootScope_, _$compile_, _$window_) {
$window = _$window_;
$rootScope = _$rootScope_;
$compile = _$compile_;
});
});
Maybe you could create a custom wrapper mock around $httpBackend that handles your special needs.
In detail, Angular overwrites components of the same name with a last-come first-served strategy, this means that the order you load your modules is important in your tests.
When you define another service with the same name and load it after the first one, the last one will be injected instead of the first one. E.g.:
apptasticMock.service("socket", function($rootScope){
this.events = {};
// Receive Events
this.on = function(eventName, callback){
if(!this.events[eventName]) this.events[eventName] = [];
this.events[eventName].push(callback);
}
// Send Events
this.emit = function(eventName, data, emitCallback){
if(this.events[eventName]){
angular.forEach(this.events[eventName], function(callback){
$rootScope.$apply(function() {
callback(data);
});
});
};
if(emitCallback) emitCallback();
}
});
This service offers the exact same interface and behaves exactly like the original one except it never communicates via any socket. This is the service we want to use for testing.
With the load sequence of angular in mind, the tests then look like this:
describe("Socket Service", function(){
var socket;
beforeEach(function(){
module('apptastic');
module('apptasticMock');
inject(function($injector) {
socket = $injector.get('socket');
});
});
it("emits and receives messages", function(){
var testReceived = false;
socket.on("test", function(data){
testReceived = true;
});
socket.emit("test", { info: "test" });
expect(testReceived).toBe(true);
});
});
The important thing here is that module('apptasticMock') gets executed after module('apptastic'). This overwrites the original socket implementation with the mocked one. The rest is just the normal dependency injection procedure.
This article I wrote could be interesting for you, as it goes into further details.
Related
I'm having difficulties understanding how to resolve the problem of the unit testing the controller, which makes a GET call during initialisation.
When testing the controller method, that performs the POST request, because of the initial GET call I'm getting the following error in my tests:
Error: Unexpected request: GET
The main part of the controller looks like this:
.controller('someController', function($scope, $http, $log) {
$scope.posts = [];
$scope.content = '';
$scope.read = function() {
$http.get('/read.php')
.success(function(data) {
$scope.posts = data;
})
.error(function(data, status, headers, config) {
throw new Error('Something went wrong with reading data');
});
};
$scope.read();
$scope.write = function() {
$http({
method: 'POST',
url: '/write.php',
data: "content=" + $scope.content,
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
})
.success(function(data) {
$scope.posts.push({ id : data.id, task : $scope.content })
})
.error(function(data, status, headers, config) {
throw new Error('Something went wrong with writing data');
});
};
});
As you can see I'm calling the read() method right after its definition - so that all records are fetched from the database on page load.
I've tried the same with the .config() or other service, but obviously the result is the same.
Now - my test is as follow:
describe('someController tests', function() {
var $scope,
$http,
$httpBackend,
$log;
beforeEach(function() {
module('myApp');
inject(function($rootScope, _$http_, _$httpBackend_, _$log_) {
$scope = $rootScope.$new();
$http = _$http_;
$httpBackend = _$httpBackend_;
$log = _$log_;
});
$httpBackend.expectGET("/mod/read.php").respond({});
});
it('should add new record', inject(function($controller) {
$controller('ToDoController', {
$scope : $scope,
$http : $http,
$log : $log
});
$scope.posts = [];
$scope.content = 'Some content';
$httpBackend
.whenPOST('/write.php')
.respond({ id : 1, content : $scope.content });
$scope.write();
$httpBackend.flush();
expect($scope.posts.length).toBe(1);
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
}));
});
Now - there are a few questions I have about this code.
Is calling the method within the controller on page load considered a good practice? I know that if I used ng-init within the document to call it - this would solve all my problems, but I just don't like this approach.
In the beforeEach() method of the test I'm calling:
$httpBackend.expectGET("/mod/read.php").respond({});
to reflect the call of the read() method during initialisation, but again - I'm not sure this is the right thing to do here. I know that if I remove it - the 'should add new record' test will fail with the error mentioned above.
I know that afterEach() should be placed outside of the it() block, but if I do this - it will cause the same problem, but error will apply to all of the tests within this describe block. Is putting it inside of the it() block considered a bad practice / incorrect?
How would I perform the read() test - if the call is already triggered in the beforeEach() loop?
Lastly - what would your suggestion be to improve / rewrite the controller and the test to make sure it works its best?
I'm pretty new to the TDD - so I'm sure it's just my limited understanding of the topic, but would appreciate some constructive help.
I am attempting to perform a unit test on this controller.
angular.module('app.dashboard', [])
.controller('DashboardController', ['$scope', 'myAppService'], function($scope, myAppService) {
var _data = myAppService.requests.get(function() {
$scope.requests = _data.requests;
});
});
myAppService is a service based on ngResource.
I want to test for the number of requests. I spent all day figuring out how to get $httpBackend injected, now I'm hung up on properly measuring the data.
beforeEach(inject(function ($controller, $rootScope, $injector) {
$httpBackend = $injector.get('$httpBackend');
$httpBackend.when('GET', '/api/requests').respond(
{requests: [{sender: 'joe', message: 'help'}, {sender: 'larry', message: 'SOS'}]}
);
});
it('should have a properly working Dashboard controller', inject(function($rootScope, $controller, $httpBackend) {
var $scope = $rootScope.$new();
var ctrl = $controller('DashboardController', {
$scope : $scope
});
expect($scope.requests.length).toBe(2);
}));
Any assistance would be greatly, massively appreciated.
For the most part, everything looks ok. There are two things that I think might be missing from your code though.
First, this method doesn't look quite right:
var _data = myAppService.requests.get(function() {
$scope.requests = _data.requests;
});
If you are providing a callback function, shouldn't that be passing in the data:
myAppService.requests.get(function(_data) {
$scope.requests = _data.requests;
});
It is hard to tell without seeing your myAppService code (that is using $resource).
Second, in order to flush through the ajax response you need to call $httpBackend.flush(); before you make your assertion with expect:
var $scope = $rootScope.$new();
var ctrl = $controller('DashboardController', {
$scope : $scope
});
httpBackend.flush();
expect($scope.requests.length).toBe(2);
From the $httpBackend docs:
flush(count):
Flushes all pending requests using the trained responses.
Hope this helps.
I started to write unit tests for my angular app.
However it seems to me that I use a lot of boilerplate code to init and test the controller.
In this Unit Test I want to test if a model from the scope is sent to the Api when I execute a function.
I needed 20 lines of code for this. This makes it inconvenient to write unit tests that do only one thing.
Do you have any tips on getting the code size to a smaller chunk?
This is my current unit test:
'use strict';
describe('controllers', function(){
beforeEach(module('kronos'));
describe('CustomerSignupCtrl', function() {
it('should send customer to Api on submit', inject(function($controller) {
var scope = {};
var $location = {};
var Api = {
signupCustomer: function(customer) {
expect(customer).toEqual({attrs: "customerdata"});
return {
success: function() { return this; },
error: function() { return this; }
};
}
};
var ctrl = $controller('CustomerSignupCtrl', {
$scope: scope,
$location: location,
Api: Api});
scope.customer = {attrs: "customerdata"};
scope.signup();
}));
});
});
What I don't like in particular are the following points
I need to init the all dependencies and it doesn't matter if I use them or not
The Api returns a promise that I only need because the controller is expecting the promise
I need to init the controller.
How can I make this code shorter and more explicit?
Edit: I just noticed I can ignore the $location Service for this unit test. Great
Edit2: I found out about angular-app, which serves as a good practice example app. There you can find specs with jasmine, which are really nice written.
Use another beforeEach method in your describe scope to set up scope, $location, controller etc, then just change them in your test as you need to. Js is dynamic so all should be fine.
You can also extract each object that you set up into a function so that you can reinitialise them in a test if you need to.
describe('controllers', function(){
beforeEach(module('kronos'));
describe('CustomerSignupCtrl', function() {
var controller, scope, $location, Api;
beforeEach(function(){
scope = {};
$location = {};
Api = {
signupCustomer: function(customer) {
expect(customer).toEqual({attrs: "customerdata"});
return {
success: function() { return this; },
error: function() { return this; }
};
}
};
controller = makeController();
})
function makeController(){
inject(function($controller){
controller = $controller('CustomerSignupCtrl', {
$scope: scope,
$location: location,
Api: Api});
});
}
it('should send customer to Api on submit', function() {
scope.customer = {attrs: "customerdata"};
scope.signup();
});
});
});
You can not shorten your code much. Things like initialization, mocking and assertion have to be done at some place. But you can improve the readability of your code by decoupling initialization and test code. Something like this:
describe('CustomerSignupCtrl', function(){
var controller, scope, location, api;
beforeEach(module('kronos'));
// initialization
beforeEach(inject(function($controller, $rootScope, $location, Api){
scope = $rootScope.$new();
location = $location;
api = Api;
controller = $controller('CustomerSignupCtrl', {
$scope: scope, $location: location, Api: api});
}));
// test
it('should send customer to Api on submit', function() {
scope.customer = {attrs: "customerdata"};
spyOn(api,'signupCustomer').andCallFake(function(customer) {
return {
success: function() { return this; },
error: function() { return this; }
};
});
scope.signup();
expect(api.signupCustomer).toHaveBeenCalledWith(scope.customer);
});
});
I have been trying to test a service to no avail for some time now and was hoping for some help. Here is my situation:
I have a service looking a little like this
myModule.factory('myService', ['$rootScope', '$routeParams', '$location', function($rootScope, $routeParams, $location) {
var mySvc = {
params: {}
}
// Listen to route changes.
$rootScope.$on('$routeUpdate', mySvc.updateHandler);
// Update #params when route changes
mySvc.updateHandler = function(){ ... };
...
...
return mySvc;
}]);
And I want to mock the services injected into 'myService' before the service gets injected into my tests so I can test the initialization code below
var mySvc = {
params: {}
}
// Listen to route changes.
$rootScope.$on('$routeUpdate', mySvc.updateHandler);
I am using Jasmine for tests and mocks. This is what I came up with for now
describe('myService', function(){
var rootScope, target;
beforeEach(function(){
rootScope = jasmine.createSpyObj('rootScope', ['$on']);
module('myModule');
angular.module('Mocks', []).service('$rootScope', rootScope );
inject(function(myService){
target = myService;
});
});
it('should be defined', function(){
expect(target).toBeDefined();
});
it('should have an empty list of params', function(){
expect(target.params).toEqual({});
});
it('should have called rootScope.$on', function(){
expect(rootScope.$on).toHaveBeenCalled();
});
});
This doesn't work though. My rootscope mock is not replacing the original and the Dependency Injection doc is confusing me more than anything.
Please help
I would spy on the actual $rootScope instead of trying to inject your own custom object.
var target, rootScope;
beforeEach(inject(function($rootScope) {
rootScope = $rootScope;
// Mock everything here
spyOn(rootScope, "$on")
}));
beforeEach(inject(function(myService) {
target = myService;
}));
it('should have called rootScope.$on', function(){
expect(rootScope.$on).toHaveBeenCalled();
});
I've tested this in CoffeScript, but the code above should still work.
You could create a RootController and then inject it:
inject(function(myService, $controller){
target = myService;
$controller('rootController', {
$scope : $rootScope.$new(),
$rootScope : myService
});
});
With this approach you can access $rootScope functions from your 'myService';
Such 'myService.$on()'
I just made it, let me know if help is needed.
I am trying to test a controller which uses angular's $resource.
function PermissionsCtrl($scope, $resource, $cookies) {
var Object = $resource('/v1/objects/:id');
loadObjects();
function loadObjects() {
$scope.myAppObjects = new Array();
var data = AppObject.get({find: '{"app_id": '+wtm.app_id+'}'},
function(){
if (data.results) {
for(var i = 0; i< data.results.length; i++) {
if(!data.results[i].is_deleted) {
(function(i){
$scope.objects(data.results[i]);
}(i));
}
}
}
},
function(error){console.log(error);});
}
And here is the test code.
var apiServer = "...";
var app_id = 999
var mock_object_data = {...};
describe('apps permissionsCtrl', function(){
var scope, ctrl, $httpBackend;
// Create a matcher for comparing data
beforeEach( function() {
this.addMatchers({
toEqualData: function(expected) {
return angular.equals(this.actual, expected);
}
});
});
// Create the controller with injected data/services
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller, $resource) {
$httpBackend = _$httpBackend_;
// cookie data to inject
var cookies = new Array();
cookies['id'] = '...'; // just needs to be declared
$httpBackend.expectGET(apiServer+'/v1/app_objects? find=%7B%22app_id%22:'+app_id+'+%7D&access_token=' + cookies['id'])
.respond( mock_object_data );
var $injector = angular.injector(['ng', 'ngResource']);
var $resource = $injector.get('$resource');
scope = $rootScope.$new();
ctrl = $controller(PermissionsCtrl, {$scope: scope, $cookies: cookies, $resource: $resource});
}));
it('should put object data into $scope', function() {
$httpBackend.flush();
expect(scope.objects).toEqualData( mock_object_data );
});
});
When I run this I get
Error: Unknown provider: $resourceProvider <- $resource
at the line where I try to create my controller. I don't understand how to inject this into my controller and no matter what I try I get the same error. A couple things I've tried are
Declaring a an empty mock object and passing it through similar to my cookies variable. I figure this is probably a bad solution anyway since I actually want to use the service.
Mimicking the scope mock and passing it into my inject function and passing $resource.$new() to my controller.
Doing nothing and hoping that httpBackend would cover it since that's what ultimately gets called anyway. Vojta Jína made it sound like that would work but no dice.
Mild epithets. Satisfying but not very effective.
After more reading and more experimenting it seems the right way to do this is to abstract the use of $resource out of the controller. In my case I wrote a service that relies on $resource and then inject that service into my controller. Meanwhile I test that service separately from my controller. Better practice all around.
My service declaration:
angular.module('apiModule', ['localResource', 'ngCookies'])
.factory('apiService', function($resource, $cookies) {
and in my unit tests I pass it through in a beforeEach setup function
beforeEach(module('apiModule'));
Try to instantiate $resource with the following code:
var $injector = angular.injector(['ng', 'ngResource']);
var $resource = $injector.get('$resource');
Similar for other services, except when they are in other modules still. Then add that module to the array.
More info