Unit testing controllers (Jasmine/Angular) that run code when the page loads - angularjs

I'm attempting to unit test some angular js controllers that I have written within the jasmine testing framework. I've got everything set up so that I am able to create instances of my controller, and pass in mock services.
However, I've got a few lines of code that run when the page loads.
$scope.tags = [];
$scope.noData = false;
$scope.generateSearchResults = function(input){
searchAPI.executeSearch(input).then(function(res){
$scope.tags = res.data;
});
};
//does some post processing on tags
$scope.checkNumberOfResults = function(){
if($scope.tags.length < 1){
$scope.noData = true;
}
}
//this is the code that runs when the page loads.. normally I want this behavior,
//but for my jasmine unit tests, I don't want the controller running any code on
//instantiation
$scope.$watch('$viewContentLoaded', function(){
$scope.searchQuery = $routeParams.query; //grabs from the url
$scope.generateSearchResults($scope.searchQuery){
.then(function(res){
$scope.checkNumberOfResults();
});
});
So if you can tell, when the page loads I want to grab a query string from the url and then display search results. The thing is, I don't want this code to be run while testing, at least not for my unit tests. Maybe when I do some integration tests I will want to be able to simulate a page load, but for now, I want to unit test some of my other functions in the controller without necessarily requiring a call to the search API service..
Does that make sense? Does anyone have any advice for where to go on this?

Instead of watching for the $viewContentLoaded event you could inject the searchQuery into your controller by using resolve of ngRoute or uiRouter. This allows you to mock the searchQuery in your tests and be independent of page loading and $routeParams
// code for ngRouter
...
controller: 'MyCtrl',
resolve: {
searchQuery: function ($route) {
// grabs search query from URL
return $route.current.params.query;
}
}
Then inject searchQuery into your controller:
module.controller('MyCtrl', function ($scope, searchQuery) {
$scope.$watch('searchQuery', function(newValue){
// ignore undefined etc.
if (!newValue) { return; }
$scope.generateSearchResults($scope.searchQuery){
.then(function(res){
$scope.checkNumberOfResults();
});
});
});

Related

AngularJS testing with Jasmine

I'm a newbie to programming and I'm trying to figure out how to unit test angularJS code with jasmine, and its driving me insane!
This is the angular code im trying to test, its all set up on an asp.net web application using abpBoilerplate and angular. The result of the code below is that when a button is clicked on the web page, a 'success' popup appears and "true" appears in a text box, if the service is available. The service is being pulled from classes within a web api project.
(function() {
var controllerId = 'app.views.home';
angular.module('app').controller(controllerId, [
'$scope', 'abp.services.lfcservice.webapi', function($scope,lfcServices) {
var vm = this;
//Home logic...
vm.CheckLfcIsAvailable = function () {
lfcServices.lfcIsAvailable()
.success(function () {
abp.notify.info('Success');
vm.Available = 'True';
});
};
I just need to know how to write a jasmine test that passes when it expects a true value for the lfc service. Ive tried loads of different combinations with no success, I could paste in 10 different attempts ive had in here but they are all very different.
Any help would be much appreciated!
First, you need to know how to test a controller, mocking the service.
Then, you need to mock the service API to return a promise.
let's say thet the controller is initiated with Available = false;.
Test an angular 1.x controller (see jsFiddle):
describe("app.views.home controller spec", function() {
var ctrl;
//depend on the module
beforeEach(module('app'));
beforeEach(inject(function($controller) {
//use angular's "$controller" to get the controller
ctrl = $controller("app.views.home");
}));
it("available should be false", function() {
expect(ctrl.Available).toBe(false);
});
});
Now, let's asume that the service returns a simple result (without promises) and see how do we provide a mock service instead of the real service.
Test an angular 1.x controller with mock service (see jsFiddle):
beforeEach(module(function($provide) {
var mockService = jasmine.createSpyObj('mock', ['lfcIsAvailable']);
mockService.lfcIsAvailable.and.returnValue(true);
$provide.value('abp.services.lfcservice.webapi', mockService);
}));
Now, let's see how to mock a promise response. for this we will use $q.
Mock angular 1.x promise (see jsFiddle):
it('should change after promise resolved', inject(function($q, $rootScope) {
//create promise
var deferred = $q.defer();
//mock service response
mockService.lfcIsAvailable.and.returnValue(deferred.promise);
//call CheckLfcIsAvailable ()
ctrl.CheckLfcIsAvailable ();
expect(ctrl.Available).toBe(false);
deferred.resolve(true);
//not yet...
expect(ctrl.Available).toBeNull(false);
//from angular $q documentation:
//"it's important to know that the resolution of promises is tied to the digest cycle"
$rootScope.$apply();
//now!
expect(ctrl.Available).toBe(true);
}));

Angular Testing DOM after update by Factory

Ok, I'm trying to test the outcome of a function that updates the DOM>
I have a directive that loads a template via url.
Then a controller calls a factory method to update the html table with data.
I have the tests showing that I can get the data that is all good.
but how can I test that the updates to the table have taken place?
I am using NodeJS with Karma and Jasmine.
I have followed tutorials on how to load in templates, and I have that working, I can load and access the templates in my test fine.
but when I run the method to update the table, the tests fail.
I'll give an scaled down example of what I'm trying to do. Note, this is just demo code, Not a working app.
Template.
<table><tr><td class="cell1"></td></tr></table>
Directive.
dataTable.directive('dataTable', function () {
return {
restrict: 'E',
templateUrl: 'path/to/template/dataTable.html'
};
});
Controller
dataTable.controller('dataTableController', ['$scope', 'dataTableFactory',
function ($scope, dataTableFactory){
$scope.updateTable = function(){
dataTableFactory.loadData();
// code to load data from dataTableFactory here! //
dataTableFactory.updateTable();
}
}])
Factory
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
"tableData": _tableData,
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
Unit Test
describe('dataTable Tests', function () {
var scope, element, $compile, mDataTableFactory, controller, tableData, doc, factory;
beforeEach(module('dataTable'));
beforeEach(module('app.templates')); // setup via ng-html2js
beforeEach(inject(function (_$rootScope_, _$compile_,_$controller_,_dataTableFactory_) {
scope = _$rootScope_.$new();
doc = _$compile_('<flood-guidance></flood-guidance>')(scope);
factory = _dataTableFactory_;
controller = _$controller_('dataTableController', {
$scope: scope,
$element: doc,
dataTableFactory: factory
});
scope.$digest();
}));
it("Template should contain the cell cell1", function(){
expect(doc.find('.cell1').contents().length).toBe(0);
expect(doc.find('.cell1').html()).toBeDefined();
});
// Passes fine, so I know the template loads ok.
it('Should show data in cell1',function(){
factory.tableData = {data: 'someData'};
scope.updateTable();
expect(doc.find('.cell1').contents().length).toBe(1);
expect(doc.find('.cell1').html()).toBe('SomeData');
});
});
});
Test Ouptut
Expected 0 to be 1. Expected '' to be 'someData'.
If I put the updateTable code in to the controller and call the update function there, the test passes, but I'd like to have this in a factory, how can I make this test pass (the app runs and works as expected, I just can't get a working test).
I understand this kind of testing is more focused on the UI and not exactly 'Unit Testing' but is it possible to do this?
So essentially updateTable cannot find the changes performed by factory.tableData. I guess the problem may be due to the way how your factory exposes the _tableData property.
Could you try to modify your factory like this:
dataTable.factory('dataTableFactory',['$document',function($document){
var _tableData;
return(
getTableData: function() { return _tableData; },
setTableData: function(newVal) { _tableData = newVal; },
loadData: function(){
// code to get data and populate _tableData.
}
updateTable: function(){
$document.find('.cell1').append(this.tableData.data);
}
)
}])
and then of course use the setter/getter accordingly. See if it works this way.
OK so I'm still not sure if I fully get your intention but here is a fiddle with my refactored example.
http://jsfiddle.net/ene4jebb/1/
First of all the factory shouldn't touch the DOM, that's the directives responsibility. Thus my rework passes the cellvalue (new scope property) to the directive, which renders it. Now when you call setTableData (which will change _tableData.data) and since in test environment call the $digest loop yourself, the directive will automatically redraw the new stuff.
Controller is kept thin as possible thus only providing a scope property to the factory.
As said not sure if you were after this, but hope it helps. If there are any questions just ask.

How to mock service in angularAMD with karma/jasmine?

I have a project using AngularAMD/RequireJS/Karma/Jasmine, that I have the basic configuration all working, most unit tests run and pass successfully.
I cannot get a mocked service injected correctly using either angular.mock.module or angularAMD.value().
I have:
// service definition in services/MyService.js
define(['app'],
function(app) {
app.factory('myService', [ '$document', function($document) {
function add(html) {
$document.find('body').append(html);
}
return { add: add };
}]);
}
);
// test
define(['angularAMD', 'angular-mocks', 'app', 'services/MyService'],
function(aamd, mocks, app) {
describe('MyService', function() {
var myBodyMock = {
append: function() {}
};
var myDocumentMock = {
find: function(sel) {
// this never gets called
console.log('selector: ' + sel);
return myBodyMock;
}
};
var svc;
beforeEach(function() {
// try standard way to mock a service through ng-mock
mocks.module(function($provide) {
$provide.value('$document', myDocumentMock);
});
// hedge my bets - try overriding in aamd as well as ng-mock
aamd.value('$document', myDocumentMock);
});
beforeEach(function() {
aamd.inject(['myService',
function(myService) {
svc = myService;
}]);
});
it('should work', function() {
// use svc expecting it to have injected mock of $document.
spyOn(myDocumentMock, 'find').andCallThrough();
spyOn(myBodyMock, 'append');
svc.add('<p></p>');
expect(myDocumentMock.find).toHaveBeenCalledWith('body');
expect(myBockMock.append).toHaveBeenCalledWith('<p></p>');
});
});
}
);
Does anyone know where I'm going wrong ? Any help would be much appreciated.
Angular isn't asynchronous, I think is not a good ideia use both. If you're trying to reach to a good modularization method, okay, but use the RequireJS optimizer to build everything before you put this on your browser, and about the tests, I think you can just use RequireJS optimizer to build your modules before, it will let you free from "CommonJS environment even in tests".
Looks like it'll be an issue with variable scopes, karma is very finicky about that. I think you should initialize your mock objects globally, then set them in the beforeEach.
The top line of my test files always looks something like:
var bodyMock, svcMock, foo, bar
Then in the beforeEach'es I set the values
Edit: Since bodyMock is only a scope variable, at the point where the tests are actually running and the browser is looking for an object 'bodyMock', it can't find anything.

Jasmine: Backbone.history.loadUrl Breaks Unrelated Tests

I've found the only way to navigate to different URLs to do view and router behavior tests is to use Backbone.history.loadUrl(). Backbone.history.navigate('#something', true) and router.navigate('#something, {trigger: true, replace: true} and any combination thereof do not work within the test. My application does NOT use pushstate.
This works correctly within the context of a single test.
describe('that can navigate to something as expected', function(){
beforeEach(function() {
this.server = sinon.fakeServer.create();
//helper method does my responds to fetches, etc. My router in my app is what starts Backbone.history
this.router = initializeBackboneRouter(this.server, this.fixtures, false);
});
afterEach(function(){
this.server.restore();
Backbone.history.stop();
Backbone.history.loadUrl('#');
});
it('should create the view object', function(){
Backbone.history.loadUrl('#something');
expect(this.router.myView).toBeDefined();
});
});
During testing you can see that backbone is appending hashes as expected to the URL: localhost:8888/#something Depending on the test.
Unfortunately, loadUrl seems to be introducing a lot of inconsistencies in the way the tests behave. During one of my tests that involves some asynchronous JS where I need to wait for an AJAX call to complete, the fails about 50% of the time with a timeout or sometimes Expected undefined to be defined. If I console out the data I expect to be there it is, so I know it's not a BS test.
it('should add the rendered html to the body', function(){
runs(function(){
Backbone.history.loadUrl('#something');
});
waitsFor(function(){
var testEl = $('#title');
if(testEl.length > 0){ return true; }
}, 1000, 'UI to be set up');
runs(function(){
var testEl = $('#title');
expect(testEl.text()).toEqual(this.router.model.get(0).title);
});
});
The important note here is that it only fails when all tests are run; run by itself it passes 100% of the time.
My question then is: is Backbone.history.loadUrl a bad way to do programatic navigation around a backbone app in jasmine? I feel like I've tried everything to get this to simulate a user going to a specific URL. Is my teardown incorrect? I've tried it without the Backbone.history.loadUrl('#'); and got different behavior but not passing tests.
The core problem seems to be that in the context of several, hundreds, or even a few jasmine tests, Backbone.history is not clearing itself out and is sticking around as one instance of itself instead of being completely re-initialized at each test.
This sucked.
The solution was to edit my code a bit to add a loading complete flag that was set to true when i was sure that the DOM was 100% finished loading.
Then I wrote a helper function that waited for that flag to be true in my beforeEach function in the root of each test.
var waitForLoadingComplete = function(view){
waitsFor(function(){
if(view.loadingComplete == true){return true;}
}, 100, 'UI Setup Finished');
}
After that I refactored my setup into a helper function:
var setupViewTestEnvironment = function(options) {
var temp = {};
temp.server = sinon.fakeServer.create();
temp.router = initializeBackboneRouter(temp.server, options.fixture);
waitForLoadingComplete(temp.router.initialview);
runs(function(){
Backbone.history.loadUrl(options.url);
temp.view = temp.router[options.view];
temp.model = temp.router[options.model];
waitForLoadingComplete(temp.view);
});
return temp;
}
Example use:
beforeEach(function() {
this.testEnv = setupViewTestEnvironment({
url: '#profile',
view: 'profileIndex',
model: 'myModel',
fixture: this.fixtures
});
});
After which I had a view that i had loaded which I could be assured was finished loading so I could test stuff on the DOM or anything else I wanted to do.

AngularJS: Accessing scope in E2E test

I'm trying to access $scope's within an E2E test without success...
As a test I tried this: (My site does not use JQuery..)
The runner has my site in a nested iframe, so I'm accessing it directly, then getting all ng-scopes and trying .scope() on them as in this post and code below...
var frameDocument = document.getElementById('test-frames').children[0].contentDocument;
var scopeElements = frameDocument.getElementsByClassName('ng-scope');
var scopes = [].map.call(scopeElements, function (e) {
return angular.element(e).scope();
});
The above code finds the proper elements, but calling scope() on them returns undefined for each....
Can someone confirm or deny that we can access the scope in E2E? I'd assume there is a way?
Thank-you
Here is my trick based on previous answer.
You can extend it to dynamic scopes. The main part is getting the reference to appWindow from addFutureAction.
//HTML CODE
<body id="main-controller" ng-controller="mainCtrl" ng-init="__init__()">
//Scenario helper.
/*
Run `callback` with scope from `selector`.
*/
angular.scenario.dsl('scope', function() {
return function(selector, callback) {
return this.addFutureAction(
'Executing scope at ' + selector,
function(appWindow, $document, done) {
var body = appWindow.document.getElementById(selector)
var scope = appWindow.angular.element(body).scope()
callback(scope)
scope.$apply()
done(null, 'OK');
})
}
})
//Actual test.
it(
'When alerts are defined, they are displayed.',
function() {
scope('main-controller', function(scope) {
scope.alerts.push({'type': 'error', 'msg': 'Some error.'})
})
expect(element('#alerts').css('display')).toEqual('block')
})
In E2E tests, accessing scope that way is not good option. Instead You can use helper functions like element() to select elements in page, and use expect() to check model data.
What you might need is unit testing. You can access $scope in unit tests easily.
There is a very good guide here: http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-testacular.html
Also it might be a timing issue, i can reach scopes in testacular runner like this. It runs tests in iframe. To make it work you need to add sleep(3) to your test. But this is very fragile.
setTimeout(function () {
console.log('try to reach frames');
var frame = window.frames[0].window.frames['senario_frame'];
if (!frame) {
console.log('too late');
} else {
var scopeElements = frame.document.getElementsByClassName('ng-scope');
var scopes = [].map.call(scopeElements, function (e) {
return frame.angular.element(e).scope();
});
console.log(scopes);
}
}, 2000);

Resources