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.
Related
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?
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.
I have a set of tests that are spread across 2 modules.
The first module has tests and for it's dependencies i declare mocks to test it without any influence from it's dependent module, like so:
beforeEach(function(){
angular.mock.module(function ($provide) {
$provide.value("mockServiceFromModule1", mockServiceFromModule1);
$provide.value("mockServiceFromModule2", mockServiceFromModule2);
});
angular.module('module1', []);
angular.module('module2', []);
angular.mock.module('moduleThatIAmTesting');
angular.mock.inject(function (_$rootScope_, _$q_, _$httpBackend_, ..., somethingFromTestModule) {
});
})
The second module has a series of tests and all of them pass when i run only them.
beforeEach(function(){
angular.mock.module('module1');
angular.mock.inject(function (_$rootScope_, _$q_, _$httpBackend_, ..., somethingFromModule1) {
});
})
Both tests when run with f(Running only them) works, but when i run the whole test suit i get errors, specially regarding module declaration or $httpBackend.
How can i make jasmine run each test as if they were the only tests?
It seems i am messing with the angular/modules/$httpBackEnd on each test and the changes are being propagated when it starts a new test.
Update 1
I have a jsFiddle representing the issue .
The structure of the problem is :
Some test is ran with a mock dependant module
Later another test wants to test the actual mocked module
Since the first moldule was already loaded we can't overwritte it and the test fails.
On the JSFiddle the error about $httpBackend without nothing to flush is because the request for the expectedGet is never hit, and it's never hit because of the previously loaded empty module
It's important to notice that the ORDER of the tests is the only thing relevant to failing as in this JSFiddle with the same tests they pass.
I could of course make a tests order and bypass this but i am aiming to find a way to do the tests with isolation without worrying about other tests side effects.
The problem you are experiencing is due to the nature of how the $compileProvider handles a new directive being registered with a pre-existing name.
In short; You are not overriding your old directive, you are creating a secondary one with the same name. As such, the original implementation runs and tries to grab baz.html and $httpBackend throws as you have not set up an expectation for that call.
See this updated fiddle that did two changes from your original fiddle.
Do not inject the parentModule to your childModule spec. That line is not needed and it is part of the reason you are seeing these errors. Oh and angular.module is evil in the land of tests. Try to not use it.
Decorate the original directive if you wish to roll with the same name as the original one, or name it something else. I've opted for naming it something else in the fiddle, but I have supplied code at the end of my answer to show the decorator way.
Here's a screenshot of what happens in the following scenario:
Module A registers a directive called baz.
Module B depends on module A.
Module B registers a directive called baz.
As you can probably imagine, in order for the module system to not insta-gib itself by letting people overwrite eachothers directives - the $compileProvider will simply register another directive with the same name. And both will run.
Take this ng-click example or this article outlining how we can leverage multiple directives with the same name.
See the attached screenshot below for what your situation looks like.
The code on lines 71 to 73 is where you could implement solution #2 that I mentioned in the start of my answer.
Decorating baz
In your beforeEach for testModule, replace the following:
$compileProvider.directive('baz', function () {
return {
restrict: 'E',
template: '{{value}}<div ng-transclude></div>',
controllerAs: 'bazController',
controller: function ($scope, fooService) {
$scope.value = 'baz' + fooService.get()
},
transclude: true
};
});
With this:
$provide.decorator('bazDirective', function ($delegate) {
var dir = $delegate[0];
dir.template = '{{value}}<div ng-transclude></div>';
dir.controller = function ($scope, fooService) {
$scope.value = 'baz' + fooService.get();
};
delete dir.templateUrl;
return $delegate;
});
jsFiddle showing the decorator approach
What about the call to angular.module('parent', [])?
You should not call angular.module('name', []) in your specs, unless you happen to be using the angular-module gist. And even then it's not doing much for you in the land of testing.
Only ever use .mock.module or window.module, as otherwise you will kill your upcoming specs that relate to the specified module, as you have effectively killed the module definition for the rest of the spec run.
Furthermore, the directive definition of baz from parentModule will automatically be available in your testModule spec due to the following:
angular.module('parent', []).directive('baz', fn());
angular.module('child', ['parent']);
// In your test:
module('child'); // Will automatically fetch the modules that 'child' depend on.
So, even if we kill the angular.module('parent', []) call in your spec, the original baz definition will still be loaded.
As such, the HTTP request flies off due to the nature of $compileProvider supporting multiple directives with the same name, and that's the reason your spec suite is failing.
Also, as a last note; You are configuring undefined modules in your beforeEach blocks. If the goal is to configure the module of your test, you are in the wrong.
The syntax is as follows:
mock.module('child', function ($compileProvider, /** etc **/) {
});
// Not this:
mock.module('child');
mock.module(function ($compileProvider, /** etc **/) {
});
This can be seen in the screenshot I posted. The $$moduleName property of your mocked baz definition is undefined, whereas I am assuming you would want that to be child.
I am a bit new to AngularJs. I am using Angular UI bootstrap (0.10.0) for modal implementation. I am getting the following errors while testing a modal controller
using AngularJs 1.2.7: TypeError: Attempted to assign to a readonly property
using AngularJs 1.2.12: unknown provider: $modalInstanceProvider <- $modalInstance.
I have gone through a lot of similar questions but couldn't understand what's the problem.
As pointed in the comments by Mahery, $modalInstance is made available for injection in the controller by AngularUI Bootstrap implementation. So, we don't need any effort to "resolve" or make it available somehow.
Modal Window Issue (Unknown Provider: ModalInstanceProvider)
This is my main controller that leads to creation of modal instance on clicking open on the partial page.
var SomeCtrl = function($scope, $modal){
$scope.open = function(){
$scope.modalInstance = $modal.open({
templateUrl: '/templates/simpleModal.html',
controller: 'simpleModalController',
});
$scope.modalInstance.result.then(
function(){
console.log("clicked OK");
},
function(){
console.log("clicked Cancel");
});
};
};
someCtrl.$inject = ["$scope", "$modal"];
angular.module('angularApp').controller("someCtrl", SomeCtrl);
This is the modal controller I wish to test if it contains the necessary functions (which I intend to add later)
(function(){
var SimpleModalController = function($scope, $modalInstance){
$scope.ok = function(){
$modalInstance.close('ok');
};
$scope.cancel = function(){
$modalInstance.dismiss('cancel');
};
};
SimpleModalController.$inject = ["$scope", "$modalInstance"];
angular.module('angularApp').controller("simpleModalController", SimpleModalController);
})();
This is the test I have written for the modal controller
describe('Testing simpleModalController',function() {
var ctrlScope;
var modalInstance;
var ctrl;
beforeEach(function() {
module('angularApp');
inject(function($rootScope, $modalInstance, $controller) {
ctrlScope = $rootScope.new();
modalInstance = $modalInstance;
ctrl = $controller('simpleModalController',
{
$scope : ctrlScope,
$modalInstance : modalInstance
});
});
});
it('should check existence of scope variables and functions when created', function() {
console.log('pending test');
});
});
I have no troubles in the functionality of modal in the app, testing the main controller and integration of modal. But I am unable to test the modal controller. I think the problem is with injection of $modalInstance in the test (of simple modal controller). But as mentioned earlier, angular-ui bootstrap makes it available.
Any help is appreciated. Thanks.
Having the next Modal Controller definition:
angular.module('module').controller('ModalInstanceController',ModalInstanceController);
function ModalInstanceController($timeout, $modalInstance, $scope) {
//controller across a bunch of modals
$scope.closeModal = function(){
$modalInstance.dismiss('cancel');
};
$scope.action = function(){
$modalInstance.dismiss();
};
}
You can create an spy Object with the needed methods using jasmine, and pass that object to the controller when you create the instance:
beforeEach(inject(($controller, $timeout, $rootScope) => {
modalInstance = jasmine.createSpyObj('modalInstance', ['dismiss']);
scope = $rootScope.$new();
controller = $controller('ModalInstanceController', {
$modalInstance: modalInstance,
$scope: scope
});
}));
Later in your test scenarios you can check the spied object:
it('should defined the required methods on the scope', () => {
expect(scope.closeModal).toBeDefined();
expect(scope.action).toBeDefined();
scope.closeModal();
expect(modalInstance.dismiss).toHaveBeenCalledWith('cancel');
scope.action();
expect(modalInstance.dismiss).toHaveBeenCalledWith();
});
So.. This is one way of testing it..
describe('Testing',function() {
it('test',function() {
inject(function($rootScope, $modal) {
var fakeModal = { };
//Basically, what you want is for your modal's controller to get
//initalized and then returned to you, so the methods in it can be unit tested
spyOn(modal, 'open').andReturn(fakeModal);
ctrl = $controller('Controller',
{
$scope : ctrlScope,
$modal: modal
});
});
});
});
I've been fighting with this issue too. The problem is that the controller your trying to instantiate in your test is a total different instance the $modal service instantiates internally passing in the actual modal window as $modalInstance. See the js code comments at the example http://angular-ui.github.io/bootstrap/#/modal:
// Please note that $modalInstance represents a modal window (instance) dependency.
// It is not the same as the $modal service used above.
So how do you test your controller then? I've not found the solution yet, im sorry. The diffulty resides in that you can't have access to your controller's scope as the $modal service creates a new scope from whether is the $rootScope or a scope you pass in with your options. So you loose track.
The least you can do is test the functions you passed in to the result promise. This is done by spying on the $modal.open function and returning a mock. And this is shown here https://stackoverflow.com/a/21370703/2202143. And complement with integration tests using tools like protractor.
Take a look at the answer that was selected as correct on the question: Unit testing a modalInstance controller with Karma / Jasmine.
I was struggling with the same issue for some time, and that question (and answer) helped me test my modals (and functions to open/close them) in a really clean way!
I didn't like any answer given here so I am adding my own.
the reason I didn't like the answers above is that they don't hold once a project is a bit bigger.
for me, the solution was to simply implement an angular service called $modalInstance...
so under spec I create a folder named shims that I use for small items such as these. (make sure to add it to karma.conf)
and there I implement
angular.module(..).service('$modalInstance', function(){
this.dismiss = jasmine.createSpy('$modalInstance.dismiss');
...
});
I find this method much cleaner and much more maintainable and straightforward.
sometimes, I like making sure my shim is only loaded for specific tests, in that case I simply give it a specific module name and then I have to add a module call for it otherwise it won't load.
I also highly recommend using a different library for modals, I recommend ng-dialog for many reasons but in this context I can say it is much more test friendly, been using it for a while now.