AngularJS controller unit testing with $http.get() on load - unit-testing

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.

Related

Angular Unit Testing a resource request

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.

Testing asynchrone function gives Unexpected request

The unittest:
"use strict";
var usersJSON = {};
describe("mainT", function () {
var ctrl, scope, httpBackend, locationMock,
beforeEach(module("testK"));
beforeEach(inject(function ($controller, $rootScope, $httpBackend, $location, $injector) {
scope = $rootScope.$new();
httpBackend = $httpBackend;
locationMock = $location;
var lUrl = "../solr/users/select?indent=true&wt=json",
lRequestHandler = httpBackend.expect("GET", lUrl);
lRequestHandler.respond(200, usersJSON);
ctrl = $controller("mainT.controller.users", { $scope: scope, $location: locationMock});
httpBackend.flush();
expect(scope.users).toBeDefined();
}));
afterEach(function () {
httpBackend.verifyNoOutstandingRequest();
httpBackend.verifyNoOutstandingExpectation();
});
describe("method test", function () {
it('should test', function () {
expect(true).toBeFalsy();
});
});
});
controller I'm testing (working):
Asynchrone function in init who's giving me trouble (uses ../solr/users/select?indent=true&wt=json):
$scope.search = function () {
var lStart = 0,
lLimit = privates.page * privates.limit;
Search.get({
collection: "users",
start: lStart,
rows: lLimit)
}, function(records){
$scope.users= records.response.docs;
});
};
What I think happens:
1. inform backend what request he will receive
2. inform backend to response on that request with empty JSON
3. create a controller (Search.get get's executed)
4. inform backend to receive all requests and answer them (flush)
Yet I always get the following error:
Error: Unexpected request: GET : ../solr/users/select?indent=true&wt=json
Am I not handling the asynchrone search function well? how should this be done?
That's not really a "unit" test, it's more of a behavioral test.
This should really be a few tests:
Test your service Search.get to make sure it's calling the proper URL and returning the result.
Test your controller method to make sure it's calling Search.get
Test your controller method to make sure it's putting the result in the proper spot.
The code you've posted is a little incomplete, but here are two unit tests that should cover you:
This is something I've blogged about extensively, and the entries go into more detail:
Unit Testing Angular Controllers
Unit Testing Angular Services
Here's an example of what I'm talking about:
describe('Search', function () {
var Search,
$httpBackend;
beforeEach(function () {
module('myModule');
inject(function (_Search_, _$httpBackend_) {
Search = _Search_;
$httpBackend = _$httpBackend_;
});
});
describe('get()', function () {
var mockResult;
it('should call the proper url and return a promise with the data.', function () {
mockResult = { foo: 'bar' };
$httpBackend.expectGET('http://sample.com/url/here').respond(mockResult);
var resultOut,
handler = jasmine.createSpy('result handler');
Search.get({ arg1: 'wee' }).then(handler);
$httpBackend.flush();
expect(handler).toHaveBeenCalledWith(mockResult);
$httpBackend.verifyNoOutstandingRequest();
$httpBackend.verifyNoOutstandingExpectation();
});
});
});
describe('myCtrl', function () {
var myCtrl,
$scope,
Search;
beforeEach(function () {
module('myModule');
inject(function ($rootScope, $controller, _Search_) {
$scope = $rootScope.$new();
Search = _Search;
myCtrl = $controller('MyCtrl', {
$scope: scope
});
});
});
describe('$scope.foo()', function () {
var mockResult = { foo: 'bar' };
beforeEach(function () {
//set up a spy.
spyOn(Search, 'get').andReturn({
then: function (fn) {
// this is going to execute your handler and do whatever
// you've programmed it to do.. like $scope.results = data; or
// something.
fn(mockResult);
}
});
$scope.foo();
});
it('should call Search.get().', function () {
expect(Search.get).toHaveBeenCalled();
});
it('should set $scope.results with the results returned from Search.get', function () {
expect(Search.results).toBe(mockResult);
});
});
});
In a BeforeEach you should use httpBackend.when instead of httpBackend.expect. I don't think you should have an assertion (expect) in your BeforeEach, so that should be moved to a separate it() block. I also don't see where lRequestHandler is defined. The 200 status is sent by default so that is not needed. Your httpBackend line should look like this:
httpBackend.when("GET", "/solr/users/select?indent=true&wt=json").respond({});
Your test should then be:
describe("method test", function () {
it('scope.user should be defined: ', function () {
expect(scope.user).toEqual({});
});
});
Your lUrl in the unit test, shouldn't be a relative path, i.e., instead of "../solr/users/select?indent=true&wt=json" it should be an absolute "/solr/users/select?indent=true&wt=json". So if your application is running at "http://localhost/a/b/index.html", lUrl should be "/a/solr/...".
Note that you can also use regular expressions in $httpBackend.expectGET(), that could be helpful here in case you are not entirely sure how the absolute path will look like later on.

Angularjs Unit Testing: Am I doing it right?

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);
});
});

AngularJS inject service mock inside service tests

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.

How to mock an function of an Angular service in Jasmine

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.