I am unit testing some Angular directives in Jasmine and running the tests with Karma in the PhantomJS headless browser.
I have a couple hundred specs. Since I've started testing the directives, I noticed that PhantomJS takes up a huge chunk of memory after running through the whole test suite a couple of times. I have a hunch that this is because the directives (and perhaps other garbage) isn't being freed up after they have been tested.
I do have some afterEach statements that remove the "compiled" directives after use, but it seems this is not enough, or I am not doing it correctly.
example.spec.js
describe('leftNavigation', function() {
var nlElement = angular.element('<div left-navigation></div>');
beforeEach(module('app'));
beforeEach(module('path/to/leftNavigation.html'));
beforeEach(inject(function($templateCache, _$compile_, _$rootScope_) {
template = $templateCache.get('full/path/to/leftNavigation.html');
$templateCache.put('full/path/to/leftNavigation.html', template);
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should contain the text BLAH', function() {
var element = $compile(nlElement)($rootScope);
$rootScope.$digest();
expect(element.text()).toContain('BLAH');
});
// Teardown
afterEach(function() {
(nlElement).remove();
});
});
Am I tearing down the right variable (nlElement), or do I need to teardown element instead? Do I need afterEach statements for every beforeEach statement?
EDIT: The directive is defined as so:
'use strict';
// Grab ref to app module
var app = angular.module('app');
app.directive('leftNavigation', ['$log',
function ($log) {
return {
restrict: 'A',
replace: true,
templateUrl: 'path/to/template.html',
scope:{
selection: '#'
}
};
}
]); //end of directive
EDIT 2: The directive's HTML template does include AngularJS expressions and ng-class attributes on multiple elements.
Related
I am not sure if what I am doing is completely wrong, but when I switched from "directive" to "components" for defining a few of my HTML elements, I suddenly broke all of my Karma tests. here's what I have:
karam.conf.js
...
preprocessors: {
'module-a/module-a.view.html': ['ng-html2js'],
...,
'module-z/module-z.view.html': ['ng-html2js']
},
ngHtml2JsPreprocessor: {
moduleName: 'theTemplates'
},
...
module-a.component.js
(function(){
"use strict";
angular.module('ModuleA').component('moduleAComponent',{
controller: 'ModuleAController as moduleAVm',
templateUrl: 'module-a/module-a.view.html'
});
})();
module-a-tests.js
"use strict";
describe('ModuleA',function(){
beforeEach(module('ModuleA'));
describe('Controller',function(){
...
});
describe('Component',function(){
var element, $rootScope;
beforeEach(module('theTemplates'));
beforeEach(inject([
'$compile','$rootScope',
function($c,$rs) {
$rootScope = $rs;
element = $c('<module-a-component></module-a-component>')($rootScope);
$rootScope.$digest(); // ???
}
]));
it('should have moduleAVm',function(){
expect(element.html()).not.toBe(''); // FAILS HERE
expect(element.html()).toContain('moduleVm'); // FAILS HERE TOO
});
});
});
The Error:
Expected '' not to be ''.
OK, after reading Angular's documentation more thoroughly, I came across this statement:
The easiest way to unit-test a component controller is by using the
$componentController that is included in ngMock. The advantage of this
method is that you do not have to create any DOM elements. The
following example shows how to do this for the heroDetail component
from above.
And it dawned on me, my describe('Controller',function(){...}); was what I really needed to change, and that I should just get rid of the 'Component' portion, formally known as 'Directive'
Here's my 'Controller' now:
beforeEach(inject([
'$componentController', // replaced $controller with $componentController
function($ctrl){
ctrl = $ctrl('queryResults',{ // Use component name, instead of controller name
SomeFactory:MockSomeFactory,
SomeService:MockSomeService
});
}
]));
Now, I still get to test my controller, while simultaneously testing the component. And I no longer have to create DOM elements using $compile, $rootScope, etc.
I am still very new to unit testing, and to be honest, there isn't anything that I could even think of testing, but I cannot build my app unless I have at least 1 test case, so I attempted to make the most simple test case I could, on the smallest block of code in the controller, and it doesn't seem to be working.
I believe it's an error in my test case, and not in my controller's code itself, because when I view my app in the browser with grunt serve the console shows no errors.
This is the error it gives me:
PhantomJS 2.1.1 (Linux 0.0.0) Controller: MainCtrl should attach a list of jackpot to the scope FAILED
/home/elli0t/Documents/Yeoman Projects/monopoly/app/bower_components/angular/angular.js:3746:53
forEach#[native code]
forEach#/home/elli0t/Documents/Yeoman Projects/monopoly/app/bower_components/angular/angular.js:323:18
loadModules#/home/elli0t/Documents/Yeoman Projects/monopoly/app/bower_components/angular/angular.js:3711:12
createInjector#/home/elli0t/Documents/Yeoman Projects/monopoly/app/bower_components/angular/angular.js:3651:22
workFn#/home/elli0t/Documents/Yeoman Projects/monopoly/app/bower_components/angular-mocks/angular-mocks.js:2138:60
TypeError: undefined is not an object (evaluating 'scope.jackpot') in /home/elli0t/Documents/Yeoman Projects/monopoly/test/spec/controllers/main.js (line 20)
/home/elli0t/Documents/Yeoman Projects/monopoly/test/spec/controllers/main.js:20:17
PhantomJS 2.1.1 (Linux 0.0.0): Executed 1 of 1 (1 FAILED) ERROR (0.04 secs / 0.007 secs)
This is my test case:
it('should attach a list of jackpot to the scope', function () {
expect(scope.jackpot.length).toBe(2);
});
And this is the block of code I'm attempting to run the test on:
var countInJackpot = localStorageService.get('jackpot');
$scope.jackpot = countInJackpot || [
{
letter: '$',
prize: '$1,000,000 Cash',
numbers: ['$611A','$612B','$613C','$614D','$615E','$616F','$617G','$618F'],
count: [0,0,0,0,0,0,0,0]
},
{
letter: '?',
prize: '$500,000 Vacation Home',
numbers: ['?619A','?620B','?621C','?622D','?632E','?624F','?625G','?626H'],
count: [0,0,0,0,0,0,0,0]
}
];
For the time being, I really just want to write 1 simple test case, so it will let me build the app. I'm currently studying unit testing, but I still don't feel ready to write more complex test cases on my own. I will save that for later.
I have included the entire contents of the files in a gist for reference, if needed, and I can include the contents of the karma.conf.js if necessary.
My gist
Within your test case, scope should be $scope?
OR
You probably haven't setup your testing environment to load in your controller.
Here is an example of mine on testing a controller... Angular makes the setup a little iffy to learn, But once you understand the flow. It's pretty great :)
I'm going to try and add as many comments to explain each piece as I can... but let me know if your need clarification. You might be using jasmine, but keep in mind, this is mocha, im using the angular mock library loaded in via the karma.conf.
describe('myController', function() {
var $scope,
createController;
// Runs before each test. Re-extantiating the controller we want to test.
beforeEach(inject(function($injector) {
// Get hold of a scope (i.e. the root scope)
$scope = $injector.get('$rootScope');
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
createController = function() {
// Creates the controller instance of our controller.
// We are injecting $scope so we will have access to it
// after the controllers code runs
return $controller('myCtrl', {
'$scope': $scope
});
};
}));
describe('#myFunction', function() {
it('jackpot should contain two objects', function() {
expect($scope.jackpot.length).to.equal(2);
});
});
});
Hope that helped. Here's some of the resources I used to learn :) Good Luck!
https://quickleft.com/blog/angularjs-unit-testing-for-real-though/
http://jaketrent.com/post/run-single-mocha-test/
I would expect you'd want to test both cases of the localStorageService having and not having data. To do so, create a spy for localStorageService (see Spies) and write your tests like this...
'use strict';
describe('Controller: MainCtrl', function () {
var scope, localStorageService, localData;
beforeEach(function() {
localData = {};
module('monopolyApp');
localStorageService = jasmine.createSpyObj('localStorageService', ['get', 'set']);
localStorageService.get.and.callFake(function(key) {
return localData[key];
});
inject(function($rootScope) {
scope = $rootScope.$new();
});
});
it('assigns jackpots from local storage if present', inject(function($controller) {
localData.jackpot = 'whatever, does not matter';
$controller('MainCtrl', {
$scope: scope,
localStorageService: localStorageService
});
expect(localStorageService.get).toHaveBeenCalledWith('jackpot');
expect(scope.jackpot).toBe(localData.jackpot);
}));
it('assigns jackpots from default array if none present in local storage', inject(function($controller) {
$controller('MainCtrl', {
$scope: scope,
localStorageService: localStorageService
});
expect(localStorageService.get).toHaveBeenCalledWith('jackpot');
expect(scope.jackpot.length).toEqual(2);
// maybe include some other checks like
expect(scope.jackpot[0].letter).toEqual('$');
expect(scope.jackpot[1].letter).toEqual('?');
}));
});
Preconditions: I am using Karma to run Jasmine unit tests against my Angular.js app modules.
My app uses the following pattern to expose modules (services/directives/controllers):
simple.js
'use strict';
export default (angular) => {
angular.module('simple', [])
.directive('simple', [function() {
return {
restrict: 'E',
replace: true,
template: '<h1>COMPILED!</h1>'
};
}]);
};
The corresponding unit test for the above example looks like this:
simple.test.js
import angular from 'angular';
import mocks from 'angular-mocks';
import simpleModule from './simple';
describe('simple', () => {
var $compile,
$rootScope;
// Load the simple module, which contains the directive
beforeEach(() => {
let simpleComponent = new simpleModule(angular);
// module('simple') raises a TypeError here
// I have also tried angular.module('simple') which
// I am pretty sure is incorrect.
});
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject((_$compile_, _$rootScope_) => {
// The injector unwraps the underscores (_) from around the
// parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('Replaces the element with the appropriate content', () => {
// Compile a piece of HTML containing the directive
var element = angular.element("<simple>not compiled</simple>");
var compiledElement = $compile(element)($rootScope);
// fire all the watches, so the scope expressions evaluate
$rootScope.$digest();
// Check that the compiled element contains the templated content
expect(element.html()).toContain("COMPILED!");
});
});
Problem: when running the above test with Karma in a web browser, the test fails and the element does not appear to be getting compiled.
What am I missing?
I can see in your code that you are missing the creation of the new $scope before do the $compile. You should do something like this:
it('Replaces the element with the appropriate content', () => {
// Compile a piece of HTML containing the directive
var scope = $rootScope.$new(); // create a new scope
var element = angular.element("<simple>not compiled</simple>");
var compiledElement = $compile(element)(scope);
// fire all the watches, so the scope expressions evaluate
scope.$digest();
// Check that the compiled element contains the templated content
expect(element.html()).toContain("COMPILED!");
});
I suspect you are not importing the directive correctly. Have you tried:
beforeEach(module('simple'));
The alternate version you indicated you tried are not correct or are patterns I have not seen:
beforeEach(() => {
let simpleComponent = new simpleModule(angular);
});
module('simple');
angular.module('simple');
The obvious is that you are using Javascript ES6 in your tests. Jasmine only understands ES5 as of now.
Try using this Jasmine ES6 Compiler, or write your tests in ES5.
Note: That your tests are not being compiled but they fail seems contradictory. They can only fail if karma attempts to run them. And they can only run if they are compiled. Are you sure about this?
As stated in the title of the question, I am getting undefined when calling isolateScope(). I tried to Unit Test my Angular Directive based on the instructions given on Unit Testing AngularJS Directives With External Templates.
Here is my code:
directive.js
angular.module("myTestApp")
.directive("testDirective", function(){
return{
restrict: "E",
require: "ngModel",
scope:{
ngModel: "=",
label: "#?"
},
templateUrl: "/mypath/templates/testDirective.html",
link: function($scope, element, attributes, ngModelCtrl){
$scope.functions = {},
$scope.settings = {
label: $scope.label ? $scope.label : ""
}
}
};
});
I have used karma-ng-html2js-preprocessor for templateUrl.
karma.conf.js (Code contains only the parts that are concerned with ng-html2js)
files:[
//All Js files concerning directives are also included here...
'/mypath/templates/*.html'
],
preprocessors: {
'/mypath/templates/*.html': [ng-html2js']
},
ngHtml2JsPreprocessor: {
moduleName: 'template'
},
plugins: ['karma-*'],
directiveSpec.js
describe("testDirective Test", function(){
var scope, $compile,$httpBackend, template;
beforeEach(module('myTestApp'));
beforeEach(module('template'));
beforeEach(inject(function($rootScope, _$compile_, _$httpBackend_){
scope = $rootScope.$new();
$compile = _$compile_;
$httpBackend = _$httpBackend_;
}));
it("should check if the value of label attribute id set to dummyLabel", function(){
$httpBackend.expect('GET','/mypath/templates/testDirective.html').respond();
scope.label = 'dummyLabel';
var element = angular.element('<test-directive label= "label" ></test-directive>');
element = $compile(element)(scope);
console.log(element);
scope.$digest();
console.log('Isolate scope: '+ element.isolateScope());
expect(element.isolateScope().label).toEqual('dummyLabel');
});
});
Here the console.log(element); prints {0: <test-directive label="label" class="ng-scope"></test-directive>, length: 1}
Problem: console.log('Isolate scope: '+ element.isolateScope()); gives undefined.
I looked on this problem on many questions in StackOverflow but was not able to find the correct solution.
Also, I have to use the $httpBackend to get the html file or else it throws Unexpected Request error.
I would be really grateful for any help since I am stuck on this error since past one week!
Just a guess here at first glance but...
Your .respond() needs to return something. Right now you are intercepting the request to your template and not responding with anything, thus resulting in empty data and isolateScope() being undefined.
For someone who might have the same problem:
The directive testing do not work as expected when using the $httpBackend to mock the HTML files. It should be and it is possible to include HTML files by simply using the ng-html2js plugin. So I removed the $httpBackend calls and used stripPrefix to properly include my HTML files as modules. It worked!
Hope it helps someone else!
I'm having trouble unit-testing directives that make use of templateUrl.
There's this awesome tutorial about AngularJS testing [1] and it even has a Github repo that goes with it [2]
So I forked it [3] and made the following changes:
On directives.js I created two new directives:
.directive('helloWorld', function() {
return {
restrict: 'E',
replace: true,
scope:{},
template: '<div>hello world!</div>',
controller: ['$scope', function ($scope) {}]
}
})
.directive('helloWorld2', function() {
return {
restrict: 'E',
replace: true,
scope:{},
templateUrl: 'mytemplate.html',
controller: ['$scope', function ($scope) {}]
}
})
and I changed test/unit/directives/directivesSpecs.js so that it loads a template into $templateCache and then added two more tests for the new directives:
//
// test/unit/directives/directivesSpec.js
//
describe("Unit: Testing Directives", function() {
var $compile, $rootScope, $templateCache;
beforeEach(angular.mock.module('App'));
beforeEach(inject(
['$compile','$rootScope', '$templateCache', function($c, $r, $tc) {
$compile = $c;
$rootScope = $r;
//Added $templateCache and mytemplate
$templateCache = $tc;
$templateCache.put('mytemplate.html', '<div>hello world 2!</div>');
}]
));
//This was already here
it("should display the welcome text properly", function() {
var element = $compile('<div data-app-welcome>User</div>')($rootScope);
expect(element.html()).to.match(/Welcome/i);
});
//Added this test - it passes
it("should render inline templates", function() {
var element = $compile('<hello-world></hello-world>')($rootScope);
expect(element.text()).equal("hello world!");
});
//Added this test - it fails
it("should render cached templates", function() {
var element = $compile('<hello-world2></hello-world2>')($rootScope);
expect(element.text()).equal("hello world 2!");
});
});
The last test fails because Angular won't compile the template like it should.
$ grunt test:unit
Running "karma:unit" (karma) task
INFO [karma]: Karma v0.10.10 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 35.0.1916 (Linux)]: Connected on socket ChISVr0ZABZ1fusdyv3m
Chrome 35.0.1916 (Linux) Unit: Testing Directives should render cached templates FAILED
expected '' to equal 'hello world 2!'
AssertionError: expected '' to equal 'hello world 2!'
Chrome 35.0.1916 (Linux): Executed 18 of 18 (1 FAILED) (0.441 secs / 0.116 secs)
Warning: Task "karma:unit" failed. Use --force to continue.
Aborted due to warnings.
I'm pretty sure this was supposed to work.
At least, it's very similar with the solution proposed by #SleepyMurth on [4].
But I feel I reached the limit of understanding what's going wrong with my current knowledge of AngularJS.
HELP! :-)
[1] http://www.yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-karma.html
[2] https://github.com/yearofmoo-articles/AngularJS-Testing-Article/
[3] https://github.com/tonylampada/AngularJS-Testing-Article
[4] Unit Testing AngularJS directive with templateUrl
The problem
When templateUrl is specified, templates are fetched using $http (even for cached templates, which $http serves from the $templateCache). For that reason, there needs to be a $digest cycle for the $http's promise to be resolved with the template content and processed by the directive.
The solution
Since promises get resolved during a $digest cycle (and since we are outside of an "Angular context"), we need to manually invoke $rootScope.$digest() before evaluating our assertions.
Thus, the last test should be modified like this:
it("should render cached templates", function() {
var element = $compile('<hello-world2></hello-world2>')($rootScope);
$rootScope.$digest(); // <-- manually force a $digest cycle
expect(element.text()).toBe(tmplText);
});
See, also, this short demo.