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.
Related
I'm new to jasmine testing. How can I test function call in watch function?
Below is my code. I'm confused about usage of spy in jasmine and how can I handle function call inside watcher.
Do I need to pause fetch() inside watch. Please suggest how to improve my testing skills.
var app = angular.module('instantsearch',[]);
app.controller('instantSearchCtrl',function($scope,$http,$sce){
$scope.$sce=$sce;
$scope.$watch('search', function() {
fetch();
});
$scope.search = "How create an array";
var result = {};
function fetch() {
$http.get("https://api.stackexchange.com/2.2/search?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!4*Zo7ZC5C2H6BJxWq&key=DIoPmtUvEkXKjWdZB*d1nw((")
.then(function(response) {
$scope.items = response.data.items;
$scope.answers={};
angular.forEach($scope.items, function(value, key) {
var ques = value.question_id;
$http.get("https://api.stackexchange.com/2.2/questions/"+value.question_id+"/answers?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!9YdnSMKKT&key=DIoPmtUvEkXKjWdZB*d1nw((").then(function(response2) {
$scope.answers[ques]=response2.data.items;
//console.log(JSON.stringify($scope.answers));
});
});
});
}
});
my test case:
describe('instantSearchCtrl', function() {
beforeEach(module('instantsearch'));
var $scope, ctrl;
beforeEach( inject(function($rootScope, $controller) {
// create a scope object for us to use.
$scope = $rootScope.$new();
ctrl = $controller('instantSearchCtrl', {
$scope: $scope
});
}));
/*var $scope = {};
var controller = $controller('instantSearchCtrl', { $scope: $scope });
expect($scope.search).toEqual('How create an array');
//expect($scope.strength).toEqual('strong');*/
it('should update baz when bar is changed', function (){
//$apply the change to trigger the $watch.
$scope.$apply();
//fetch().toHaveBeenCalled();
fetch();
it(" http ", function(){
//scope = $rootScope.$new();
var httpBackend;
httpBackend = $httpBackend;
httpBackend.when("GET", "https://api.stackexchange.com/2.2/search?page=1&pagesize=10&order=desc&sort=activity&intitle="+$scope.search+"&site=stackoverflow&filter=!4*Zo7ZC5C2H6BJxWq&key=DIoPmtUvEkXKjWdZB*d1nw((").respond([{}, {}, {}]);
});
});
});
First you should trigger the watch. For that you should change search value and after that manually run: $scope.$digest() or $scope.$apply()
In order to fully test the fetch function you should also mock the response to the second request (or the mock for all the second level requests if you want to test the iteration).
After that you should add some expect statements. For the controller code they should be:
expect($scope.items).toEqual(mockResponseToFirstRequest);
expect($scope.answers).toEqual(mockResponseCombinedForSecondLevelRequests);
As for using the spy in karma-jasmine tests, those limit the amount of the code tested. A plausible use for spy in this case is to replace the httpBackend.when with spyOn($http, 'get').and.callFake(function () {})
Here is the documentation for using spies https://jasmine.github.io/2.0/introduction.html#section-Spies
I have a controller that calls a service and sets some variables. I want to test that those variables get set to the response.
My Controller:
tankService.getCurrentStats().success(function (response) {
$scope.stats.tankAvgHours = response.tankAvgHours;
$scope.stats.stillAvgHours = response.stillAvgHours;
$scope.stats.stillRemaining = response.stillRemaining;
$scope.stats.tankRemaining = response.tankRemaining;
$scope.stats.loaded = true;
});
My Test:
...
var STATS_RESPONSE_SUCCESS =
{
tankAvgHours:8,
stillAvgHours:2,
stillRemaining: 200,
tankRemaining:50
};
...
spyOn(tankService, "getCurrentStats").and.callThrough();
...
it('calls service and allocates stats with returned data', function () {
expect($scope.stats.loaded).toBeFalsy();
$httpBackend.whenPOST('../services/tanks/RelayTankService.asmx/getCurrentStats').respond(200, $q.when(STATS_RESPONSE_SUCCESS));
tankService.getCurrentStats()
.then(function(res){
result = res.data.$$state.value;
});
$httpBackend.flush();
expect($scope.stats.tankAvgHours).toEqual(result.tankAvgHours);
expect($scope.stats.stillAvgHours).toEqual(result.stillAvgHours);
expect($scope.stats.stillRemaining).toEqual(result.stillRemaining);
expect($scope.stats.tankRemaining).toEqual(result.tankRemaining);
expect($scope.stats.loaded).toBeTruthy();
});
The result is that my scope variables are undefined and don't equal my mocked response data. Is it possible to pass the mocked values so I can test the success function correctly populates the variables?
Thanks!
Since you're testing the controller, there's no need to mock $http's POST the way you've done in the test.
You just need to mock the tankService's getCurrentStats method.
Assuming that your tankService's getCurrentStats method returns a promise, this is how your test must be:
describe('controller: appCtrl', function() {
var $scope, tankService, appCtrl;
var response = {
tankAvgHours: 8,
stillAvgHours: 2,
stillRemaining: 200,
tankRemaining: 50
};
beforeEach(module('myApp'));
beforeEach(inject(function($controller, $rootScope, _tankService_) {
$scope = $rootScope.$new();
tankService = _tankService_;
spyOn(tankService, 'getCurrentStats').and.callFake(function() {
return {
then: function(successCallback) {
successCallback(response);
}
}
});
AdminController = $controller('appCtrl', {
$scope: $scope,
tankService: _tankService_
});
}));
describe('appCtrl initialization', function() {
it('calls service and allocates stats with returned data', function() {
expect(tankService.getCurrentStats).toHaveBeenCalled();
expect($scope.stats.tankAvgHours).toEqual(response.tankAvgHours);
expect($scope.stats.stillAvgHours).toEqual(response.stillAvgHours);
expect($scope.stats.stillRemaining).toEqual(response.stillRemaining);
expect($scope.stats.tankRemaining).toEqual(response.tankRemaining);
expect($scope.stats.loaded).toBeTruthy();
});
});
});
Hope this helps.
I think you're testing the wrong thing. A service should be responsible for returning the data only. If you want to test the service, then by all means mock the httpbackend and call the service, but then verify the data returned by the service, not the $scope. If you want to test that your controller calls the service and adds the data to the scope, then you need to create your controller in the test, give it scope that you create, and then test that those variables get added. I didn't test this so the syntax might be off, but this is probably the direction you want to go in.
var scope, $httpBackend, controller;
beforeEach(inject(function(_$httpBackend_, $controller, $rootScope) {
$httpBackend = _$httpBackend_;
$httpBackend.whenPOST('../services/tanks/RelayTankService.asmx/getCurrentStats').respond(200, $q.when(STATS_RESPONSE_SUCCESS));
scope = $rootScope.$new();
controller = $controller('myController', {
$scope: scope
});
}));
it('calls service and allocates stats with returned data', function () {
expect(scope.stats.loaded).toBeFalsy();
$httpBackend.flush();
expect(scope.stats.tankAvgHours).toEqual(result.tankAvgHours);
expect(scope.stats.stillAvgHours).toEqual(result.stillAvgHours);
expect(scope.stats.stillRemaining).toEqual(result.stillRemaining);
expect(scope.stats.tankRemaining).toEqual(result.tankRemaining);
expect(scope.stats.loaded).toBeTruthy();
});
We're just getting started with unit testing in our Angular app and we are using a karma/mocha/chai framework for unit testing. I carried out some basic unit tests on various services and factories we have defined and it the unit testing works great. However now we want to test some controller and evaluate the scope vars that the controllers are modifying.
Here is an example of one such controller:
angular.module('App').controller('myCtrl', ['$scope', 'APIProxy',
function ($scope, APIProxy) {
$scope.caseCounts = {caseCount1: 0, caseCount2: 0};
$scope.applyCounts = function () {
$scope.caseCounts.caseCount1 = {...some case count logic...}
$scope.caseCounts.caseCount2 = {...some case count logic...}
};
APIProxy.getAll().then(function (data) {
{...do a bunch of stuff...}
$scope.data = data;
$scope.applyCounts();
});
}]
);
Now, when I unit test I would like to start off with just a simple 'does the $scope.caseCounts have values > 0, then I'll build from there. However it is not obvious how to make the controller cause the APIProxy service run and how to handle the eventual return of data. We've tried $scope.getStatus(), and $scope.apply() and a few other things but I feel like we are way off the mark and we are fundamentally missing something about how to go about this.
Currently our controller tester looks like:
describe("myCtrl unit tests",function(){
beforeEach(module('App'));
var ctrl, APIProxy;
beforeEach(inject(function ($rootScope, $controller, _APIProxy_)
{
$scope = $rootScope.$new();
APIProxy = _APIProxy_;
ctrl = $controller('myCtrl', {$scope: $scope, APIProxy: APIProxy});
}));
it('Loads data correctly', function() {
expect(ctrl).to.not.be.undefined;
//??? what else do we do here to fire the getAll function in controller?
});
});
It's usually better to test the service and the controller separately.
To test the service, you can use $httpBackend to mock the XHR requests:
https://docs.angularjs.org/api/ngMock/service/$httpBackend
To test the controller, you can simply provide mocked values instead of the actual service when you initalize the controller
APIProxy = {'mocked':'data'};
ctrl = $controller('myCtrl', {$scope: $scope, APIProxy: APIProxy});
Or more generally, to mock any provider of your module:
module(function($provide) {
$provide.constant('ab','cd');
$provide.value('ef', 'gh');
$provide.service('myService', function() { });
});
Which will override the 'myService' referenced as dependencies in your controller (if there is one). If you need it directly, you can then inject it too:
var myService;
beforeEach(inject(function (_myService_) {
myService = _myService_;
}));
If you need APIProxy to return a promise, you can mock it too with
https://docs.angularjs.org/api/ng/service/$q
and resolve, e.g.:
var deferred = $q.defer();
deferred.resolve({'mocked':'data'});
return deferred.promise;
If you do want to test them together, you can do a spy on the API function you call and have the spy return a resolved promise.
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'm using the "controller as" syntax to create my controller. I have a private initialization function that calls a function to load the default data.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
var mc = this;
mc.dataLoaded = false;
function init() {
mc.loadData();
}
mc.loadData = function(){
mc.dataLoaded = true;
}
init();
});
In my test I'm creating a spy to check whether the loadData function has been called. Although I can verify that the function has been called by testing for the mc.dataLoaded flag, my spy doesn't seem to record the function being called. How can I get the spy to correctly record the function call?
describe('Testing a Hello World controller', function() {
var $scope = null;
var ctrl = null;
//you need to indicate your module in a test
beforeEach(module('plunker'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
spyOn($scope.mc, 'loadData').and.callThrough();
}));
it('should call load data', function() {
expect($scope.mc.loadData).toHaveBeenCalled();
//expect($scope.mc.dataLoaded).toEqual(false);
});
});
Plunker link
This sequence of lines:
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
spyOn($scope.mc, 'loadData').and.callThrough();
Means that the Jasmine spy is created after the controller has already been instantiated by $controller. Before the spy is created, the init function has already executed.
You can't switch the lines around either, because MainCtrl needs to exist before you can spy on a method on it.
If the init function calls another service, then spy on that service's method and assert that the service is called correctly. If MainCtrl is just doing something internally, then test the result of that, for example, by asserting that controller's data/properties are updated. It may not even be worth testing if it's trivial enough.
Also, since you're using the controller as syntax, you can reference the controller through the return value of calling $controller, rather than accessing the scope directly:
ctrl = $controller('MainCtrl as mc', {
$scope: $scope
});
ctrl.loadData === $scope.mc.loadData; // true
I found a solution that allowed me to avoid changing my controller. I included a $state mock service in the test suite's beforeEach method, and gave it a reload mock method:
beforeEach(inject(function ($controller, $rootScope) {
stateMock = {
reload: function() {
myCtrl = $controller('MyCtrl');
}
};
...
Then within the jasmine tests, I can simply call stateMock.reload() to re-initialize my controller while preserving my spies I declared in another beforeEach block.