How to test $scope in an AngularJS controller - angularjs

How do I test the $scope object of my controller?
Am I able to get the actual data that was attached to it?
I have my test set up as shown below, but myScope is saying undefined.
'use strict';
describe('myApp.view1 module', function() {
var $httpBackend, $rootScope, createController, jsonHandler;
beforeEach(module('myApp.view1'));
describe('view1 controller', function(){
beforeEach(inject(function($rootScope, $controller, $injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// backend definition common for all tests
$httpBackend.when('GET', 'view1/quizzes.json')
.respond({data: '[XXX,XXX,XXX]'});
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
var myScope = $rootScope.$new()
createController = function() {
return $controller('View1Ctrl', {'$scope' : myScope });
};
var controller = createController();
}));
it('should get the json files', function(){
$httpBackend.expectGET('/view1/quizzes.json');
});
it('should get the json files', function(){
expect(myScope).toBe("words");
});
});
});
controller:
myApp.controller('View1Ctrl', [
'$scope',
'$http',
function($scope, $http) {
$http.get('view1/quizzes.json')
.then(function(res){
$scope.quizzes = res.data.quizzes
})
.then(function(){
$http.get('view1/questions.json')
.then(function(res){
$scope.questions = res.data.questions
})
.then(function(){
$scope.quiz = [ [], [], [], [], [] ]
_($scope.questions).forEach(function(qu){
_($scope.quizzes).forEach(function(quiz){
if (_.includes($scope.quizzes[(quiz.id - 1)].question_ids, qu.id)){
$scope.quiz[(quiz.id - 1)].push(qu)
}
})
})
})
.then(function(){
// console.log($scope.quiz)
})
});
}
]);

OK. First let's start by cleaning up the mess a little. You don't need to use $injector to get access to services, since they can be injected using inject.
You also don't want a single test for a whole module. Your test should just test the controller. I'll show you an example testing just the first interaction with the backend.
describe('view1 controller', function() {
// declare the variables that need to be used in all tests
// the $scope contains the data and functions we want to test
var $scope;
// the $httpBackend service allows mocking the http... backend
var $httpBackend;
// the $controller service allows instantiating our controller
var $controller;
// load the module containing the component I want to test
beforeEach(module('myApp.view1'));
// prepare the tests. Use inject to access the services we need.
// To avoid a name clash with the variables defined above, we can
// enclose the actual service names into underscores
beforeEach(inject(function($rootScope, _$httpBackend_, _$controller_) {
// initialize our variables
$httpBackend = _$httpBackend_;
$controller = _$controller_;
// create a scope and initialize our $scope variable with it
$scope = $rootScope.$new();
}));
// we need to be able to instantiate our controller in our tests. Let's
// define a function that does that
function createController() {
// we initialize the controller with the scope we have created sooner
// so, the scope the controller receives as argument is our scope
// the controller will populate the scope, and we can test it has
// populated it correctly
$controller('View1Ctrl', { $scope: $scope });
}
// now let's write a simple test. The controller, when instantiated,
// should use $http to load view1/quizzes.json, and, once it gets
// the response, it should populate $scope.quizzes with the quizzes
// attribute of the JSON received as response
it('should populate the quizzes from the backend', function() {
// we first need to tell our fake backend: you should receive a
// request, and when you do, you should return this response
var data = {
quizzes: ['hello world']
};
$httpBackend.expectGET('view1/quizzes.json').respond(data);
// now we will create our controller. The controller should send
// a http request to get the quizzes. If it indeed does correctly,
// the fake backend will return the above data **when we tell it to
// do it**.
createController();
// Now, the controller has sent the request. But it hasn't received
// the response yet. Let's send the response.
$httpBackend.flush();
// now, the controller should have received the response, and should
// thus have extracted the quizzes from it and populated
// $scope.quizzes with them
expect($scope.quizzes).toEqual(data.quizzes);
});
});

Related

Use $http in a controller constructor without obtaining it as a parameter

I am new to AngularJS and I am trying to create a controller for this karma test (practicing TDD, I should not change the test):
beforeEach(inject(function($injector, $rootScope) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
$http = $injector.get('$http');
// backend definition common for all tests
apiCertificatesServerHandler = $httpBackend.when('GET', '/api/certificates/1')
.respond({"id":1,"name":"example.com","issuer":"Cert Corp.","expires":1469565860612});
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
var $rootScope = $injector.get('$rootScope');
var $rootParams = $injector.get('$rootParams');
createController = function() {
return $controller('CertificateController', {'$rootScope' : $rootScope, '$scope': $rootScope.$new() '$routeParams': {id: 1}});
};
}));
and then:
it('should fetch certificate', function() {
$httpBackend.expectGET('/api/certificates/1');
var vm = createController();
$httpBackend.flush();
expect(vm.data).toEqual({"id":1,"name":"example.com","issuer":"Cert Corp.","expires":1469565860612});
});
The only way to obtain the info via GET request in the controller, (that I can think of), is obtaining $http in the constructor. But this is not what the test expects. Here is my solution, which is not ok according to the test:
function CertificateController($http, $routeParams) {
var vm = this;
$http({
method: 'GET',
url: '/api/certificates/' + $routeParams.id
}).then(function (certificate) { ...
Can you advice me how to write the controller in order the test to pass? If I instantiate it only with $rootScope and $scope, how to obtain the $http obj that I need? Can you advice me how can I get a reference to the $http parameter in this case?

How to send mocked response data to called through $http call?

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

Confused by angular unit test

I'm testing out on of my controllers. I keep getting
Error: Expected POST /auth/signup with different data
EXPECTED: {"username":"justin","firstName":"Justin","lastName":"Young","email":"xxxx#xxx.com","company":"5579d602ba9f26a414be5d57","url":"http://www.me.com","referrer":"me#me.com"}
It's completing the post to auth/signup as expected, but the data is empty? I'm passing in sampleUserResponse into the expectations so I don't get why it's not passing that data back as the response. What am I doing wrong?
My test:
'use strict';
(function() {
// Authentication controller Spec
describe('AdminItemController', function() {
// Initialize global variables
var controller,
scope,
$httpBackend,
$stateParams,
$location;
beforeEach(function() {
jasmine.addMatchers({
toEqualData: function(util, customEqualityTesters) {
return {
compare: function(actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
// Load the main application module
beforeEach(module(ApplicationConfiguration.applicationModuleName));
// The injector ignores leading and trailing underscores here (i.e. _$httpBackend_).
// This allows us to inject a service but then attach it to a variable
// with the same name as the service.
beforeEach(inject(function($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_) {
// Set a new global scope
scope = $rootScope.$new();
// Point global variables to injected services
$stateParams = _$stateParams_;
$httpBackend = _$httpBackend_;
$location = _$location_;
// Initialize the Authentication controller
controller = $controller('AdminItemController', {
$scope: scope,
$stateParams:$stateParams
});
$httpBackend.when('GET', 'modules/core/views/home.client.view.html').respond({});
}));
it('$scope.create() with valid form data should send a POST request with the form input values and then locate to new object URL', inject(function(Users) {
// Create a sample article object
var sampleUserPostData = new Users({
username: 'justin',
firstName:'Justin',
lastName:'Young',
email:'xxxx#xxx.com',
company:'5579d602ba9f26a414be5d57',
url:'http://www.me.com',
referrer:'me#me.com'
});
// Create a sample User response
var sampleUserResponse = new Users({
_id:'4579d602ba9f26a414be5d59',
username: 'justin',
firstName:'Justin',
lastName:'Young',
email:'xxxx#xxx.com',
company:'5579d602ba9f26a414be5d57',
url:'http://www.me.com',
referrer:'me#me.com'
});
// Fixture mock form input values
//scope.title = 'An User about MEAN';
//scope.content = 'MEAN rocks!';
// Set POST response
$httpBackend.expectPOST('/auth/signup', sampleUserPostData).respond(sampleUserResponse);
// Run controller functionality
scope.addPost();
$httpBackend.flush();
// Test form inputs are reset
//expect(scope.title).toEqual('');
//expect(scope.content).toEqual('');
// Test URL redirection after the User was created
//expect($location.path()).toBe('/admin/users/' + sampleUserResponse._id);
}));
});
}());
My Simplified Controller:
.controller('AdminItemController', ['$scope', '$http', '$location','apiResource','$stateParams', '$state','$log',
function($scope, $http, $location, apiResource, $stateParams, $state, $log) {
$scope.addPost = function() {
apiResource.save({api_resource:'auth', api_action: 'signup'},$scope.item).$promise.then(function(response){
$scope.$parent.users.push($scope.item);
});
};
}
])
The problem is in your controller, the params you send with the POST is $scope.item but in your test, you DO NOT set your $scope.item to be anything. Therefore, a POST with undefined params will be sent (because $scope.item is undefined). Moreover, in your test, you expect the params sent to equal to sampleUserPostData. Apparently it will fail because undefined !== sampleUserPostData. What you can do is just to set the scope.item = sampleUserPostData; before expectPOST and it will be fine.
Working fiddle.

AngularJs: Test service with Jasmine

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.

mock angular service/promise in a karma/jasmine test

I'm trying to write a karma/jasmine test and I would like some explanations about how mocks are working on a service which is returning a promise. I explain my situation :
I have a controller in which I do the following call :
mapService.getMapByUuid(mapUUID, isEditor).then(function(datas){
fillMapDatas(datas);
});
function fillMapDatas(datas){
if($scope.elements === undefined){
$scope.elements = [];
}
//Here while debugging my unit test, 'datas' contain the promise javascript object instead //of my real reponse.
debugger;
var allOfThem = _.union($scope.elements, datas.elements);
...
Here is how my service is :
(function () {
'use strict';
var serviceId = 'mapService';
angular.module('onmap.map-module.services').factory(serviceId, [
'$resource',
'appContext',
'restHello',
'restMap',
serviceFunc]);
function serviceFunc($resource, appContext, restHello, restMap) {
var Maps = $resource(appContext+restMap, {uuid: '#uuid', editor: '#editor'});
return{
getMapByUuid: function (uuid, modeEditor) {
var maps = Maps.get({'uuid' : uuid, 'editor': modeEditor});
return maps.$promise;
}
};
}
})();
And finally, here is my unit test :
describe('Map controller', function() {
var $scope, $rootScope, $httpBackend, $timeout, createController, MapService, $resource;
beforeEach(module('onmapApp'));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$rootScope = $injector.get('$rootScope');
$scope = $rootScope.$new();
var $controller = $injector.get('$controller');
createController = function() {
return $controller('maps.ctrl', {
'$scope': $scope
});
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
var response = {"elements":[1,2,3]};
it('should allow user to get a map', function() {
var controller = createController();
$httpBackend.expect('GET', '/onmap/rest/map/MY-UUID?editor=true')
.respond({
"success": response
});
// hope to call /onmap/rest/map/MY-UUID?editor=true url and hope to have response as the fillMapDatas parameter
$scope.getMapByUUID('MY-UUID', true);
$httpBackend.flush();
});
});
What I really want to do is to have my response object ( {"elements:...}) as the datas parameter of the fillMapDatas function. I don't understand how to mock all the service things (service, promise, then)
So you want to test, if your service responses as expected? Then, this is something you would rather test on the service. Unit test promise based methods could look like this:
var mapService, $httpBackend, $q, $rootScope;
beforeEach(inject(function (_mapService_, _$httpBackend_, _$q_, _$rootScope_) {
mapService = mapService;
$httpBackend = _$httpBackend_;
$q = _$q_;
$rootScope = _$rootScope_;
// expect the actual request
$httpBackend.expect('GET', '/onmap/rest/map/uuid?editor=true');
// react on that request
$httpBackend.whenGET('/onmap/rest/map/uuid?editor=true').respond({
success: {
elements: [1, 2, 3]
}
});
}));
As you can see, you don't need to use $injector, since you can inject your needed services directly. If you wanna use the correct service names throughout your tests, you can inject them with prefixed and suffixed "_", inject() is smart enough to recognise which service you mean. We also setup the $httpBackend mock for each it() spec. And we set up $q and $rootScope for later processing.
Here's how you could test that your service method returns a promise:
it('should return a promise', function () {
expect(mapService.getMapUuid('uuid', true).then).toBeDefined();
});
Since a promise always has a .then() method, we can check for this property to see if it's a promise or not (of course, other objects could have this method too).
Next you can test of the promise you get resolves with the proper value. You can do that setting up a deferred that you explicitly resolve.
it('should resolve with [something]', function () {
var data;
// set up a deferred
var deferred = $q.defer();
// get promise reference
var promise = deferred.promise;
// set up promise resolve callback
promise.then(function (response) {
data = response.success;
});
mapService.getMapUuid('uuid', true).then(function(response) {
// resolve our deferred with the response when it returns
deferred.resolve(response);
});
// force `$digest` to resolve/reject deferreds
$rootScope.$digest();
// make your actual test
expect(data).toEqual([something]);
});
Hope this helps!

Resources