I put the code in a fiddle so it can be easily updated and 'worked with' if needed.
describe('PlayersListCtrl', function() { // Jasmine Test Suite
beforeEach(module('wc2014App'));
var ctrl, scope, $httpBackend;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
ctrl = $controller('PlayersListCtrl', {
$scope: scope
});
}));
it('should have an empty player array', function() {
expect(scope.players.length).toBe(0);
});
describe('PlayersListCtrl', function() {
var $httpBackend, $rootScope, createController;
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$httpBackend.when('GET', '../app/stubs/players.json').respond(
{userId: 'userX'},
{'A-Token': 'xxx'});
$rootScope = $injector.get('$rootScope');
var $controller = $injector.get('$controller');
createController = function() {
return $controller('PlayersListCtrl', {'$scope' : $rootScope });
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should fetch authentication token', function() {
$httpBackend.expectGET('../app/stubs/players.json');
var controller = createController();
$httpBackend.flush();
});
});
});
The rest, cause its quite verbose, is in the fiddle: http://jsfiddle.net/tLte2/
Basically the first test passes, not a hard one, but the second one depends on a JSON stub and gives errors like:
PhantomJS 1.9.7 (Mac OS X) PlayersListCtrl PlayersListCtrl should fetch authentication token FAILED
Error: No pending request to flush !
Cant seem to get a grip on how this $httpBackend stiff works. Is must be possible to just fire it and set the result in the scope of the controller?
--edit
Basically got everything wired up perfectly and can do some simple tests that run just fine, however getting JSON stub data in there seems to be a pain. Workaround can be just defining the array described in the the JSON on the controller scope like: controller.players = ['one','two','three',..... etc ......]
But that doesnt feel right. That $httpBackend stuff shouldn't be that hard to fix right?
Related
I have an angular app, that on initialisation, make a number of http requests.
I have set up a test, to expect the first request, and the second,
describe("MyController--", function () {
var controller, scope, $httpBackend, myFactory;
var iResponse = {
table: 'red',
price: '1.99'
};
beforeEach(angular.mock.module('myApp'));
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _myFactory_) {
scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$httpBackend.expectGET("/app/shopping/cart/layout.html").respond({});
$httpBackend.expectGET("/app/rest/products").respond(iResponse);
myFactory = _myFactory_;
spyOn(myFactory, 'getData').and.callThrough();
controller = _$controller_('MainController', {$scope: scope});
scope.$apply();
$httpBackend.flush();
}));
it('should verify that the controller exists ', function() {
expect(controller).toBeDefined();
});
With the above, i keep seeing the error:
Error: Unexpected request: GET /app/rest/products
Information: Expected GET /app/shopping/cart/layout.html
Any ideas what i am missing?
Firstly I would preload HTML in karma conf so u don't have to expect HTML
Secondly does your controller make a call to the unexpected url? If so u shud expect the request
All the best
So I have a controller like this:
angular.module('someModule').controller('someController',function(productService) {
$scope.products = [];
$scope.init = function() {
aService.fetchAll().then(function(payload) {
$scope.products = filterProducts(payload.data);
});
}
$scope.init();
function filterProducts(products) {
//for each of products filter some specific ones
return filteredProducts;
}
});
I am writing a test that will call the $scope.init() and has to verify that the products were filtered appropriately. I am mocking the $httpBackend so the code looks like this:
describe("someController", function() {
"use strict";
var $controller; //factory
var controller; //controller
var $rootScope;
var $state;
var $stateParams;
var $injector;
var $scope;
var $httpBackend;
var productService;
beforeEach(function(){
angular.mock.module("someModule")
inject(function (_$rootScope_, _$state_, _$injector_, $templateCache, _$controller_, _$stateParams_, _$httpBackend_, _productService_) {
$rootScope = _$rootScope_;
$state = _$state_;
$stateParams = _$stateParams_;
$injector = _$injector_;
$controller = _$controller_;
$httpBackend = _$httpBackend_;
productService = _productService_;
});
controller = $controller("someController", {$scope: $scope, $state: $state});
});
it("init() should filter products correctly",function(){
//Arrange
var expectedFilteredProducts = ["1","2"];
var products = ["0","1","2"];
$httpBackend.whenGET("api/products").respond(products);
//Act
$scope.init();
//Assert
setTimeout(100,function(){
expect($scope.products).toEqual(expectedFilteredProducts);
});
$httpBackend.flush();
});
});
The problem is that without the setTimeout the test doesn't pass. Is there a way to test what I am trying to do without it and without introducing complex $q/promises just for the test? As a side note productService is returning a promise $http. Thanks.
Edit: setTimeout makes test run but no assertions are happening..
Based on the comments, the problem was that the $httpBackend.flush() needs to be executed before the expectations. Effectively it resolves all requests it was trained to resolve (and their respective promises) so that expectations can run on completed promises.
It is also a good idea to use $httpBackend.verifyNoOutstandingExpectation() / $httpBackend.verifyNoOutstandingRequest() after the tests.
Also note that the controller has already called scope.init(), so calling it again in the test is redundant - and may even cause failures depending on the exact case.
Also for the setTimeout running but not asserting anything: Using it in a spec makes this spec asynchronous. You will have to define the spec with the done callback and call it from the asynchronous code, when the test is really finished, as:
it("init() should filter products correctly",function(done) {
...
//Assert
setTimeout(100, function() {
...
done();
});
});
Note again that Angular mocks provide ways to avoid using setTimeout for 99.9% of the time, even the $interval/$timeout services are properly mocked!
I did this controller
app.controller('controller',['$scope','httpServices',function($scope,httpServices){
$scope.items= undefined;
httpServices.getItems( function(items){
$scope.items= items;
});
}]);
and I wrote this test
describe('controller', function () {
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
controller = $controller('controller', {
'$scope': scope
});
}));
it('defined', function () {
expect(scope.items).toBeUndefined();
})
});
How I can test the scope.items after to have called the service?
I assume that your service httpServices is making some http requests. Therefore you should use the mock-backend service in order to test your controller.
Something like this, pay attention to the comments that I've made inside the code:
describe('Your specs', function() {
var $scope,
$controller,
$httpBackend;
// Load the services's module
beforeEach(module('yourApp'));
beforeEach(inject(function(_$controller_, $rootScope, _$httpBackend_) {
$scope = $rootScope.$new();
$httpBackend = _$httpBackend_;
$controller = _$controller_;
//THIS LINE IS VERY IMPORTANT, HERE YOU HAVE TO MOCK THE RESPONSE FROM THE BACKEND
$httpBackend.when('GET', 'http://WHATEVER.COM/API/SOMETHING/').respond({});
var createController = function(){
$controller('controller', {$scope: $scope});
}
}));
describe('Your controller', function() {
it('items should be undefined', function() {
createController();
expect(scope.items).toBeUndefined();
});
it('items should exist after getting the response from the server', function () {
//THIS LINE IS ALSO VERY IMPORTANT, IT EMULATES THE RESPONSE FROM THE SERVER
$httpBackend.flush();
expect(scope.items).toBeDefined();
});
});
});
The question title states this is to test a service, but the code of the question looks like an attempt is being made to test the controller. This answer describes how to test the controller.
If you're testing the controller that calls httpServices.getItems, then you need to mock it/stub getItems in order to
Control it on the test
Not assume any behaviour of the real httpServices.getItems. After all, you're testing the controller, and not the service.
A way to do this is in a beforeEach block (called before the controller is created) provide a fake implementation of getItems that just saves the callback passed to it.
var callback;
beforeEach(inject(function(httpServices) {
callback = null;
spyOn(httpServices, 'getItems').and.callFake(function(_callback_) {
callback = _callback_;
});
});
In the test you can then call this callback, passing in some fake data, and test that this has been set properly on the scope.
it('saves the items passed to the callback on the scope', function () {
var testItems = {};
callback(testItems);
expect($scope.items).toBe(testItems);
});
This can be seen working at http://plnkr.co/edit/Z7N6pZjCS9ojs9PZFD04?p=preview
If you do want to test httpServices.getItems itself, then separate tests are the place for that. Assuming getItems calls $http, then you are most likely to need to use $httpBackend to handle mock responses. Most likely, these tests would not instantiate any controller, and I suspect not need to do anything on any scope.
I am expending a lot of time trying to understand how the $httpBackend and the angular-translate could work together in order to test if the translation functionality still works.
I am in this point, and I really don't know how to solve this problem.
'use strict';
describe('Directive: translate', function () {
beforeEach(function () {
angular.module('myApp', ['pascalprecht.translate']);
});
var element,
$compile,
$rootScope,
$http,
$httpBackend;
beforeEach(inject(function (_$rootScope_, _$compile_, _$httpBackend_, _$http_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$http = _$http_;
$httpBackend = _$httpBackend_;
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should translate to English', function () {
element = $compile('<p translate>discover_more</p>')($rootScope);
$rootScope.$digest();
$httpBackend.expect('GET', 'langs/en.json').respond(200); // Should I return some data at this point?
$http.get('langs/en.json').then(function () {}); // Should I do something here?
$httpBackend.flush();
expect(element.html()).toBe('Discover more');
});
});
My test of course fails. The thing is that I don't know how to 1) really get the JSON with the data and 2) say the directive "here is your data, do your work".
Edit:
Ok, some light over the issue. I just was looking at the testing of this angular module (https://github.com/angular-translate/angular-translate/tree/master/test/unit/directive) and I could make it work:
'use strict';
describe('Directive: translate', function () {
beforeEach(function () {
angular.module('gajoApp', ['pascalprecht.translate']);
});
var element,
$compile,
$rootScope;
beforeEach(module('pascalprecht.translate', function ($translateProvider) {
$translateProvider
.translations('en', {
'discover_more': 'Discover more'
})
.preferredLanguage('en');
}));
beforeEach(inject(function (_$rootScope_, _$compile_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should translate to English', function () {
element = $compile('<p translate>discover_more</p>')($rootScope);
$rootScope.$digest();
expect(element.html()).toBe('Discover more');
});
});
What I would like, however, is combine this solution with the proper AJAX calls that return the JSON, to test that this is been done too.
You should return from your expected GET request whatever the angular-translate need to actually replace the discover_more in your element.
beforeEach(function () {
$httpBackend.when(
'GET',
'langs/en.json'
).respond({'discover_more': 'Discover more'});
});
Note, that I dont know the excact object that angular-translate expects, so it could differ from my suggestion. Anyway return it when the GET request is perceived.
In addition you're not supposed to make the GET request yourself within your test. If everything is set uo correctly, it should work if you add the expected return to your expected GET request.
Unfortunately it is due to restrictions on angular-translate, but you can use your real JSON locale file by either:
1) Using a plugin to load JSON files combined with $httpBackend to load your locale file when angular-translate requests it.
beforeEach(inject(function (_$httpBackend_) {
$httpBackend = _$httpBackend_;
$httpBackend.whenGET('locale-pt.json').respond(readJSON('langs/en.json'));
$httpBackend.flush();
})));
2) Overriding your app's translations with the $translateProvider.translations() method with a JSON read by a plugin to load JSON files
beforeEach(module(function ($translateProvider) {
$translateProvider.translations('en', readJSON('langs/en.json'));
}));
Note this should be below your beforeEach(module('myApp')); or you will get an $injector error.
In angular js unit test, I wanted to set xhr response to each test(in "it" method"), not in beforeEach method, but it seems not working.?
This works
describe('ExampleListCtrl', function(){
var $scope, $httpBackend;
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('examples').respond([]); // <--- THIS ONE
$controller(ExampleListCtrl, {$scope: $scope = $rootScope.$new()});
}));
it('should create "examples" with 0 example fetched', function() {
expect($scope.examples).toBeUndefined();
$httpBackend.flush();
expect($scope.examples).toEqual([]);
});
});
result
Executed 8 of 8 SUCCESS (0.366 secs / 0.043 secs)
But this fails with error when I move expectGet method to each method. I don't know why.
describe('ExampleListCtrl', function(){
var $scope, $httpBackend;
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$controller(ExampleListCtrl, {$scope: $scope = $rootScope.$new()});
}));
it('should create "examples" with 0 example fetched', function() {
$httpBackend.expectGET('examples').respond([]); // <--- MOVED TO HERE
expect($scope.examples).toBeUndefined();
$httpBackend.flush();
expect($scope.examples).toEqual([]);
});
});
Here is the error
....
at /Users/me/app/test/unit/controllers/ExampleListCtrlSpec.js:3:1
Error: No pending request to flush
at Error (<anonymous>)
at Function.$httpBackend.flush (/Users/me/app/test/lib/angular/angular-mocks.js:1171:34)
at null.<anonymous> (/Users/me/app/test/unit/controllers/ExampleListCtrlSpec.js:14:18)
---- Edited ----
Following the advice below, I have moved the controller out of beforeEach, and changed test like this, so that I can test $httpBackend.expectGET more than once.
describe('ExampleListCtrl', function(){
var $scope, $rootScope, $httpBackend;
beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
controller = $controller;
$scope = $rootScope.$new();
}));
it('should create "examples" model with 0 example fetched from xhr', function() {
$httpBackend.expectGET('examples').respond([]);
controller(ExampleListCtrl, {$scope: $scope});
$httpBackend.flush();
expect($scope.examples).toEqual([]);
});
});
This happens because once you instantiate your controller, it fetches $http and, thus, $httBackend. So, all expectations shall be made prior to requiring it.
If you wish to move your $httpBackend.expectGet to the spec (it block) then you need to move your $controller(ExempleListCtrl, ... as well, after the expectations.