In an effort to be more Angular 2.0 like I've decided to eliminate defining a controller in the traditional way:
e.g.
return {
restrict: 'E',
controller: 'MyCtrl',
controllerAs: 'vm',
bindToController: true,
scope: {},
templateUrl: 'path/to/tempate.html'
}
Here we can see that the controller name is passed as a string meaning that we have something like this defined somewhere:
app.controller('MyCtrl', function(...));
In a traditional test I'd just inject $controller service into the test to retrieve the MyCtrl controller. But I've decided to do this:
return {
restrict: 'E',
controller: MyCtrl,
controllerAs: 'vm',
bindToController: true,
scope: {},
templateUrl: 'path/to/tempate.html'
}
Here the only difference is that what is being passed to the controller declaration is a function called MyCtrl. This seems all well and good but do you go about retrieving this controller and testing it?
I tried doing this:
var $compile, $rootScope, $httpBackend, element, ctrl;
beforeEach(module('app'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_, _$httpBackend_) {
$compile = _$compile_;
$rootScope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
element = $compile('<directive></directive>')($rootScope);
ctrl = element[0].controller;
});
});
But in the above I get undefined coming back for the controller. Has anyone else made this move to be more like Angular 2.0? What differences have had to be made when it comes to testing?
Thanks
P.S The idea of the change in syntax is to make it easy to upgrade to Angular 2.0 when the time comes for us to do it.
EDIT
I've spent the last day or two trying to retrieve the controller and the only way that works seems to be just passing the controller in the traditional way. Here's a list of the ways I've tried (see the comments to determine what each outputs)
(function() {
'use strict';
/*
This part is used just for testing the html rendered
*/
describe('flRequestPasswordReset template: ', function() {
var $compile, $rootScope, $templateCache, template, element, ctrl;
var path = 'templates/auth/fl-request-password-reset/fl-request-password-reset.html';
beforeEach(module('app'));
beforeEach(module(path));
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$templateCache = _$templateCache_;
// Using nghtml2js to retrieve our templates
template = $templateCache.get(path);
element = angular.element(template);
$compile(element)($rootScope.$new());
$rootScope.$digest();
// $rootScope has a reference to vm which is what my
// controllerAs defines but only contains one of the variables
// This returns undefined
ctrl = element.controller('flRequestPasswordReset');
}));
it('should be defined', function() {
expect(element).toBeDefined();
});
});
describe('flRequestPasswordReset: template with controller: ', function() {
var $compile, $rootScope, $httpBackEnd, element, ctrl;
var path = '/templates/auth/fl-request-password-reset/fl-request-password-reset.html';
beforeEach(module('app'));
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_, _$httpBackend_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$httpBackEnd = _$httpBackend_;
$httpBackEnd.expectGET(path).respond({});
element = angular.element('<fl-request-password-reset></fl-request-password-reset>');
$compile(element)($rootScope.$new());
$rootScope.$digest();
// Uses alternate name for directive but still doesn't get the
// controller
ctrl = element.controller('fl-request-password-reset');
}));
it('should be defined', function() {
expect(element).toBeDefined();
});
});
/*
This part is used for testing the functionality of the controller by itself
*/
xdescribe('flRequestPasswordReset: just controller', function() {
var scope, ctrl;
beforeEach(module('app'));
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope;
// Instantiate the controller without the directive
ctrl = $controller('FlRequestPasswordResetController', {$scope: scope, $element: null});
}));
it('should be defined', function() {
expect(true).toBe(true);
});
/*
This works because I moved the controller back to the old way
*/
});
}());
According to my understanding of this question under pkozlowski.opensource answer you can use pass the controller as a function only if you don't encapsulate it inside the directive. This way it's exposed globally. This is a bad thing as it pollutes the global namespace however he seems to suggest wrapping it up when the application is built.
Is my understanding of this correct?
Related
function in controller:
angular.module('myApp').controller('MyController', function(){
$scope.f = function($event){
$event.preventDefault();
//logic
return data;
}
})
describe('MyController', function(){
'use strict';
var MyController,
$scope;
beforeEach(module('myApp'));
beforeEach($inject(function($rootScope, $controller){
$scope = $rootScope.$new();
MyController = $controller('MyController', {
$scope: $scope
})
}));
})
it('should...', function(){
//fire event and expect data
})
$scope.f function is used in directive, it can be executed by ng-click="f($event)"
what is right way for fire event in unit test?
Short Answer
You don't need to fire the event. You have access to the scope, which has the function you want to test. This means you just execute the function, then assert. It will look something like this:
it('should call preventDefault on the given event', function(){
var testEvent = $.Event('someEvent');
$scope.f(testEvent);
expect(testEvent.isDefaultPrevented()).toBe(true);
});
See the following:
jQuery Event Object
event.isDefaultPrevented()
Full Spec
Also - your it block should be inside your describe block, so that it has access to the $scope field. It should look more like this:
describe('MyController', function(){
'use strict';
var MyController,
$scope;
beforeEach(module('myApp'));
beforeEach($inject(function($rootScope, $controller){
$scope = $rootScope.$new();
MyController = $controller('MyController', {
$scope: $scope
})
}));
it('should call preventDefault on the given event', function(){
var testEvent = $.Event('someEvent');
$scope.f(testEvent);
expect(testEvent.isDefaultPrevented()).toBe(true);
});
})
A Note About Structure
Don't be afraid to use the describe blocks to structure your tests. Imagine you had another function on the $scope called f2, then you would probably want to partition your spec file up more like this:
describe('MyController', function(){
'use strict';
var MyController,
$scope;
beforeEach(module('myApp'));
beforeEach($inject(function($rootScope, $controller){
$scope = $rootScope.$new();
MyController = $controller('MyController', {
$scope: $scope
})
}));
describe('$scope', function() {
describe('.f()', function() {
// tests related to only the .f() function
});
describe('.f2()', function() {
// tests related to only the .f2() function
});
});
})
This has the benefit that when a test fails, the error message you see is constructed based on the hierarchy of describe blocks. So it would be something like:
MyController $scope .f() should call preventDefault on the given
event
I'm testing a directive ('planListing') that has a dependency on a service called 'planListingService'. This service has a dependency to another service called 'ajax' (don't shoot the messenger for the bad names).
I'm able to compile the directive, load its scope and get the controller WITH A CAVEAT. As of now I am being forced to mock both services 'planListingService' and 'ajax' otherwise I will get an error like this:
Error: [$injector:unpr] Unknown provider: ajaxProvider <- ajax <- planListingService
http://errors.angularjs.org/1.3.20/$injector/unpr?p0=ajaxProvider%20%3C-%20ajax%20%3C-%20planListingService
I thought that because I was mocking up the 'planListingService' that I wouldn't have to actually bother with any implementation nor any dependencies of this service. Am I expecting too much?
Here is the code in a nutshell:
planListing.js
angular.module('myApp')
.directive('planListing', planListing)
.controller('planListingCtrl', PlanListingCtrl);
function planListing() {
var varDirective = {
restrict: 'E',
controller: PlanListingCtrl,
controllerAs: 'vm',
templateUrl: "scripts/directives/planListing/planListing.html";
}
};
return varDirective;
}
PlanListingCtrl.$inject = ['planListingService'];
function PlanListingCtrl(planListingService) {
...
}
planListingService.js
angular.module('myApp')
.factory('planListingService', planListingService);
planListingService.$inject = ['$q', 'ajax'];
function planListingService($q, ajax) {
...
}
ajax.js
angular.module('myApp')
.factory('ajax', ['backend', '$browser', 'settings', '$http', '$log',
function (backend, $browser, settings, $http, $log) {
...
planListing.spec.js
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
module('myApp');
module('my.templates');
beforeEach(module(function ($provide){
// This seems to have no effect at all, why?
$provide.service('planListingService', function () {
this.getAllPricePlans=function(){};
});
// I don't get the error if I uncomment this:
// $provide.service('ajax', function ($q) {
// this.getAllPricePlans=function(){};
// });
}));
beforeEach(function() {
module('myApp');
module('my.templates');
});
beforeEach(angular.mock.inject(function (_$compile_,_$rootScope_,_$controller_){
$compile=_$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
el = angular.element('<plan-listing></plan-listing>');
scope = $rootScope.$new();
$compile(el)(scope);
scope.$digest();
ctrl = el.controller('planListing');
scope = el.isolateScope() || el.scope();
vm = scope.vm;
}));
describe('testing compilation / linking', function (){
it('should have found directive and compiled template', function () {
expect(el).toBeDefined();
expect(el.html()).not.toEqual('');
expect(el.html()).toContain("plan-listing-section");
});
});
it('should have a defined controller',function(){
expect(ctrl).toBeDefined();
});
it('should have a defined scope',function(){
expect(ctrl).toBeDefined();
});
});
So why is that I need to mock up the 'ajax' service even though I am mocking up 'planListingService' which is the one calling the 'ajax' service?
Thanks!
I have been there... feels like bad start But i think your directive is depend on the service and you need to inject it in order to directive can work with this, Just by calling directive it doesn't mean that it's going to inject it in your test. It will look for it and if it's not injected it will give you error
you could do so before testing your directive
beforeEach(inject(function ($injector) {
yourService = $injector.get('yourService');
})
For documentation purposes, here is the answer (thanks #estus for noticing this):
Indeed the problem was related to the incorrect initialization of my modules. Instead of this:
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
module('myApp');
module('my.templates');
...
I should've done this:
describe('testing planListing.js',function(){
var el,ctrl,scope,vm;
var service;
beforeEach(module('myApp'));
beforeEach(module('my.templates'));
...
After that things started working again as expected.
I am new to developing in angular, and am trying to learn how to test angular controllers. The controller I am testing uses $location.seach().something. I looked at the docs for $location, but don't quickly see how I am supposed to mock this in karma/jasmine.
The controller:
rmtg.controller('ErrorCtrl', ['Session', '$location', '$routeParams', '$scope', '$window',
function(Session, $location, $routeParams, $scope, $window) {
console.log('ErrorCtrl(%o, %o, %o)', $location.path(), $location.search(), $routeParams);
$scope.status = $location.search().status;
$scope.message = $location.search().message;
$scope.isAuthorized = (typeof(Session.auth) === 'object');
$scope.signin = function() {
$window.location = '/signin/#/' + $routeParams.origin + (Session.auth ? '?email=' + Session.auth.email : '');
};
}]);
My current spec attempt:
'user strict';
describe('Testing the errorCtrl controller', function(){
beforeEach(module("rmtg"));
var errorCtrl, scope;
beforeEach(inject(function($controller, $rootScope){
scope = $rootScope;
errorCtrl = $controller("ErrorCtrl", {
$scope: scope
});
}));
it('$scope.status should be set to 404 when location is set to 404', function(){
//set the $location.search values so that the scope is correct
$location.search('status', '404');
expect(scope.status).toBe('404');
});
});
And the current error message:
Testing the errorCtrl controller $scope.status should be set to 404 when location is set to 404 FAILED
Expected undefined to be '404'.
at Object. (/Users/adamremeeting/git/mrp-www/app/tests/example.js:20:24)
I'd also really appreciate links to resources on tdd with angular 1.5 and how I mock and stub correctly.
Edit After Answer
So I updated the test as per user2341963 suggestions, and did my best to look through his plunker example, but still don't have a passing test.
the current spec (controller has not changed from above)
'user strict';
describe('ErrorCtrl', function(){
beforeEach(module("rmtg"));
var scope, $location, $controller;
beforeEach(inject(function(_$controller_, _$rootScope_, _$location_){
scope = _$rootScope_.$new();
$location = _$location_
$controller = $_controller_;
}));
describe('$scope.status', function(){
it('should set status to 404', function(){
//set the $location.search values so that the scope is correct
$location.search('status', '404');
//init controller
$controller('ErrorCtrl', {
$scope: scope,
$location: $location
});
expect(scope.status).toBe('404');
});
});
});
But I am getting an error now that $controller is not defined.
You are getting undefined in your test because you are not setting $location anywhere.
Based on your controller, the search parameters must be set before the controller is initialised. See plunker for full example.
describe('testApp', function() {
describe('MainCtrl', function() {
var scope, $location, $controller;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$controller_, _$location_) {
scope = _$rootScope_.$new();
$location = _$location_;
$controller = _$controller_;
}));
it('should set status to 404', function() {
// Set the status first ...
$location.search('status', '404');
// Then initialise the controller
$controller('MainCtrl', {
$scope: scope,
$location: $location
});
expect(scope.status).toBe('404');
});
});
});
As for resources, so far I've found the angular docs are good enough.
Preface : I'm quite new to Angular and very new to unit-testing. Be gentle.
I'm trying to run unit tests on a controller for a bootstrap modal window.
When initiating the modal I am passing through an object called thisVersion like so :
function showShareDialog(thisVersion){
var modalInstance = $modal.open({
templateUrl: 'app/shares/views/shares-dialogue.tpl.html',
controller: 'SharesCtrl',
resolve:{
thisVersion:function(){
return thisVersion;
}
}
});
modalInstance.result.then(function (validated) {
// .. more code
});
}
As soon as the Shares Controller is instantiated I am calling a method on thisVersion
thisVersion.getList('shares').then(function(result){
$scope.shares = result;
});
thisVersion is being passed in to the controller as a dependency, and that all works as expected.
The issue, however, is that I can't seem to inject it into my test suites. I keep getting this error
Error: [$injector:unpr] Unknown provider: thisVersionProvider <- thisVersion
This is (most of) my test suite :
var scope, controller, thisVersion;
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope, _thisVersion_) {
scope = $rootScope.$new();
controller = $controller('SharesCtrl', {
$scope: scope
});
thisVersion = _thisVersion_;
}));
it('should get a list of all shares', function(){
expect(thisVersion.getList).not.toBe('undefined');
});
I know I am going to have to mock the call to the Api from thisVersion.getList() but for now I'm just concerned with getting the test suite to recognise thisVersion
You need to inject thisVersion into the $controller, seeing as how it's a resolved value in $modal.open.
var modalInstance = $modal.open({
templateUrl: 'app/shares/views/shares-dialogue.tpl.html',
controller: 'SharesCtrl',
resolve:{
// Resolved properties will be available for DI, into the controller.
thisVersion:function(){
return thisVersion;
}
}
});
beforeEach(module('app'));
beforeEach(inject(function ($controller, $rootScope, _thisVersion_) {
scope = $rootScope.$new();
controller = $controller('SharesCtrl', {
$scope: scope,
thisVersion: _thisVersion_ /** inject the service _into_ the controller **/
});
}));
I have a controller test that depends on the Angular $routeParams service:
var $routeParams, MainCtrl, scope;
beforeEach(inject(function ($controller, $rootScope, $injector, $templateCache) {
scope = $rootScope.$new();
$routeParams = $injector.get('$routeParamsMock');
MainCtrl = $controller('MainCtrl', {
$scope: scope,
$routeParams: $routeParams,
});
}));
it('should load a pg from $routeParams', function(){
scope.userData = {};
$routeParams._setPg('PG_FIRST');
scope.$digest();
timeout.flush();
expect(scope.userData.pg).toBe(0);
$routeParams._setPg('PG_SECOND');
scope.$digest();
timeout.flush();
expect(scope.userData.pg).toBe(1);
});
$routeParamsMock:
!(function(window, angular){
'use strict';
angular.module('vitaApp')
.service('$routeParamsMock', function() {
var _pg = null;
return{
pg: _pg,
_setPg: function(pg){
_pg = pg;
}
}
});
})(window, window.angular);
When debugging the test, I was surprised to find out that $routeParamsMock.pg was returning null every single time, even though I called _setPg with a different value.
Is it because null is considered a primitive (with a type of object...), and thus passed by value?, or perhaps because Angular is copying the object that is passed to the $controller service?.
The solution I am looking for is preferably one that won't require to instanciate different controllers per different test scenerios.
eg:
MainCtrl = $controller('MainCtrl', {
$scope: scope,
$routeParams: {'pg': 'PG_FIRST'},
});
MainCtrl = $controller('MainCtrl', {
$scope: scope,
$routeParams: {'pg': 'PG_SECOND'},
});
The thing is, what you don't want to do, is probably the best solution you have. A mock makes sense when what you want to mock is kinda complex. Complex dependency with methods, lot of states, etc. For a simple object like $routeParams it makes all the sense of the world to just pass a dummy object to it. Yes it would require to instantiate different controllers per test, but so what?
Structure your tests in a way that makes sense, makes it readable and easy to follow.
I suggest you something like:
describe('Controller: Foo', function() {
var $controller, $scope;
beforeEach(function() {
module('app');
inject(function($rootScope, _$controller_) {
$scope = $rootScope.$new();routeParams = {};
$controller = _$controller_;
});
});
describe('With PG_FIRST', function() {
beforeEach(function() {
$controller('Foo', { $scope: $scope, $routeParams: {'PG': 'PG_FIRST'}});
});
it('Should ....', function() {
expect($scope.something).toBe('PG_FIRST');
});
});
describe('With PG_SECOND', function() {
beforeEach(function() {
$controller('Foo', { $scope: $scope, $routeParams: {'PG': 'PG_SECOND'}});
});
it('Should ....', function() {
expect($scope.something).toBe('PG_SECOND');
});
});
});
With a good test organization, I can say that I like this test easy to follow.
http://plnkr.co/edit/5Q3ykv9ZB7PuGFMfWVY5?p=preview