I want to test an AngularJS directive declared like this
app.directive('myCustomer', function() {
return {
template: 'cust.html'
controller: 'customerController'
};
});
In the test I would like to inject (or override) the controller, so that I can test just the other parts of the directive (e.g. the template). The customerController can of course be tested separately. This way I get a clean separation of tests.
I have tried overriding the controller by setting the controller property in the test.
I have tried injecting the customController using $provide.
I have tried setting ng-controller on the html directive declaration used in the test.
I couldn't get any of those to work. The problem seems to be that I cannot get a reference to the directive until I have $compiled it. But after compilation, the controller is already set up.
var element = $compile("<my-customer></my-customer>")($rootScope);
One way is to define a new module (e.g. 'specApp') that declares your app (e.g. 'myApp') as a dependency. Then register a 'customerController' controller with the 'specApp' module. This will effectively "hide" the customerController of 'myApp' and supply this mock-controller to the directive when compiled.
E.g.:
Your app:
var app = angular.module('myApp', []);
...
app.controller('customerController', function ($scope,...) {
$scope = {...};
...
});
app.directive('myCustomer', function () {
return {
template: 'cust.html',
controller: 'customerController'
};
});
Your spec:
describe('"myCustomer" directive', function () {
$compile;
$newScope;
angular.module('specApp', ['myApp'])
/* Register a "new" customerController, which will "hide" that of 'myApp' */
.controller('customerController', function ($scope,...) {
$scope = {...};
...
});
beforeEach(module('specApp'));
it('should do cool stuff', function () {
var elem = angular.element('<div my-customer></div>');
$compile(elem)($newScope);
$newScope.$digest();
expect(...
});
});
See, also, this working demo.
I think there is a simpler way than the accepted answer, which doesn't require creating a new module.
You were close when trying $provide, but for mocking controllers, you use something different: $controllerProvider. Use the register() method in your spec to mock out your controller.
beforeEach(module('myApp', function($controllerProvider) {
$controllerProvider.register('customerContoller', function($scope) {
// Controller Mock
});
});
Related
I am trying to test my controller and service using Jasmine.
I have a home controller defined like below
angular.module('TFPMPortal').controller('HomeController', Home);
Home.$inject = ["$cookieStore", "PartsTrackHttpCallService", "$scope"];
/* #ngInject */
function Home(cookieStore, PartsTrackHttpCallService, scope) {
}
Here is my unit test using Jasmine
describe('HomeController', function () {
var scope, ctrl, cookieStore, PartsTrackHttpService;
beforeEach(module('TFPMPortal'));
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
ctrl = $controller('HomeController', {
cookieStore: null,
partsTrackHttpService: PartsTrackHttpService,
$scope: scope
});
}));
it('should call the init data function', function () {
var count = 0;
PartsTrackHttpService.httpCall("common/tfpminitdata", scope.mc.PartsTrackProfile, true, true).then(function (response) {
count = response.data.SiteList.length;
});
expect(scope.greeting).toBe("Greetings Frederik");
});
});
I am getting error - 'HomeController' is not a function, got undefined.
In each test suite (test file) you need to make sure you load the Angular module that contains the code you wish to test.
In this case, the your HomeController was declared on the module named "TFPMPortal". Load this module in your test:
beforeEach(module('TFPMPortal'));
Note that this loads the "TFPMPortal" module, but if your controller/tests are using services/factories/etc from other modules in your app, you need to load those modules too. For example, it looks like you'll need to include 'ngCookies' (b/c you use $cookieStore):
beforeEach(module('TFPMPortal', 'ngCookies'));
EDIT: Actually on further review, your test code above is fulfilling the $cookieStore dependency by passing null, so referencing the ngCookies module may not required ... on the other hand, there's no need to do this b/c Angular can inject the real $cookieStore.
I wish to reuse my mocks instead of having to set them up in every unit test that has them as dependency. But I'm having a hard time figuring out how to inject them properly.
Here's my attempt at unit test setup, which of course fails because ConfigServiceMockProvider doesn't exist.
describe('LoginService tests', function () {
var LoginService;
beforeEach(module('mocks'));
beforeEach(module('services.loginService', function ($provide, _ConfigServiceMock_) {
$provide.value("ConfigService", _ConfigServiceMock_);
/* instead of having to type e.g. everywhere ConfigService is used
* $provide.value("ConfigService", { 'foobar': function(){} });
*/
});
beforeEach(inject(function (_LoginService_) {
LoginService = _LoginService_;
});
}
ConfigServiceMock
angular.module('mocks').service('ConfigServiceMock', function() {
this.init = function(){};
this.getValue = function(){};
}
I realize I probably could have ConfigServiceMock.js make a global window object, and thereby not needing to load it like this. But I feel there should be a better way.
Try something like this:
describe('Using externally defined mock', function() {
var ConfigServiceMock;
beforeEach(module('mocks'));
beforeEach(module('services.configService', function($provide) {
$provide.factory('ConfigService', function() {return ConfigServiceMock;});
}));
beforeEach(module('services.loginService'));
beforeEach(inject(function (_ConfigServiceMock_) {
ConfigServiceMock = _ConfigServiceMock_;
}));
// Do not combine this call with the one above
beforeEach(inject(function (_LoginService_) {
LoginService = _LoginService_;
}));
it('should have been given the mock', function() {
expect(ConfigServiceMock).toBeDefined('The mock should have been defined');
expect(LoginService.injectedService).toBeDefined('Something should have been injected');
expect(LoginService.injectedService).toBe(ConfigServiceMock, 'The thing injected should be the mock');
});
});
According to this answer, you have to put all of your calls to module before all of your calls to inject.
This introduces a bit of a catch-22 because you have to have the reference to your ConfigServiceMock (via inject) into the spec before you can set it on the LoginService (done in the module call)
The work-around is to set an angular factory function as the ConfigService dependency. This will cause angular to lazy load the service, and by that time you will have received your reference to the ConfigServiceMock.
I want to test that the following function is in fact called upon the initialization of this controller using jasmine. It seems like using a spy is the way to go, It just isn't working as I'd expect when I put the expectation for it to have been called in an 'it' block. I'm wondering if there is a special way to check if something was called when it wasn't called within a scope function, but just in the controller itself.
App.controller('aCtrl', [ '$scope', function($scope){
$scope.loadResponses = function(){
//do something
}
$scope.loadResponses();
}]);
//spec file
describe('test spec', function(){
beforeEach(
//rootscope assigned to scope, scope injected into controller, controller instantiation.. the expected stuff
spyOn(scope, 'loadResponses');
);
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function(){
expect(scope.loadResponses).toHaveBeenCalled();
});
});
You need to initialise the controller yourself with the scope you've created. The problem is, that you need to restructure your code. You can't spy on a non-existing function, but you need to spyOn before the function gets called.
$scope.loadResponses = function(){
//do something
}
// <-- You would need your spy attached here
$scope.loadResponses();
Since you cannot do that, you need to make the $scope.loadResponses() call elsewhere.
The code that would successfully spy on a scoped function is this:
var scope;
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', {$scope: scope});
scope.$digest();
}));
it("should have been called", function() {
spyOn(scope, "loadResponses");
scope.doTheStuffThatMakedLoadResponsesCalled();
expect(scope.loadResponses).toHaveBeenCalled();
});
Setting the spy before controller instantiation (in the beforeEach) is the way to test controller functions that execute upon instantiation.
EDIT: There is more to it. As a comment points out, the function doesn't exist at the time of ctrl instantiation. To spy on that call you need to assign an arbitrary function to the variable (in this case you assign scope.getResponses to an empty function) in your setup block AFTER you have scope, but BEFORE you instantiate the controller. Then you need to write the spy (again in your setup block and BEFORE ctrl instantiation), and finally you can instantiate the controller and expect a call to have been made to that function. Sorry for the crappy answer initially
The only way I have found to test this type of scenarios is moving the method to be tested to a separate dependency, then inject it in the controller, and provide a fake in the tests instead.
Here is a very basic working example:
angular.module('test', [])
.factory('loadResponses', function() {
return function() {
//do something
}
})
.controller('aCtrl', ['$scope', 'loadResponses', function($scope, loadResponses) {
$scope.loadResponses = loadResponses;
$scope.loadResponses();
}]);
describe('test spec', function(){
var scope;
var loadResponsesInvoked = false;
var fakeLoadResponses = function () {
loadResponsesInvoked = true;
}
beforeEach(function () {
module('test', function($provide) {
$provide.value('loadResponses', fakeLoadResponses)
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', { $scope: scope });
});
});
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
expect(loadResponsesInvoked).toBeTruthy();
});
});
For real world code you will probably need extra work (for example, you may not always want to fake the loadResponses method), but you get the idea.
Also, here is a nice article that explains how to create fake dependencies that actually use Jasmine spies: Mocking Dependencies in AngularJS Tests
EDIT: Here is an alternative way, that uses $provide.delegate and does not replace the original method:
describe('test spec', function(){
var scope, loadResponses;
var loadResponsesInvoked = false;
beforeEach(function () {
var loadResponsesDecorator = function ($delegate) {
loadResponsesInvoked = true;
return $delegate;
}
module('test', function($provide) {
$provide.decorator('loadResponses', loadResponsesDecorator);
});
inject(function($controller, $rootScope) {
scope = $rootScope.$new();
$controller('aCtrl', { $scope: scope });
});
});
it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
expect(loadResponsesInvoked).toBeTruthy();
});
});
I didn't quite understand any of the answers above.
the method I often use - don't test it, instead test the output it makes..
you have not specified what loadResponses actually does.. but lets say it puts something on scope - so test existence of that..
BTW - I myself asked a similar question but on an isolated scope
angular - how to test directive with isolatedScope load?
if you still want to spy - on an unisolated scope, you could definitely use a technique..
for example, change your code to be
if ( !$scope.loadResponses ){
$scope.loadResponses = function(){}
}
$scope.loadResponses();
This way you will be able to define the spy before initializing the controller.
Another way, is like PSL suggested in the comments - move loadResponses to a service, spy on that and check it has been called.
However, as mentioned, this won't work on an isolated scope.. and so the method of testing the output of it is the only one I really recommend as it answers both scenarios.
I'm using bindToController in a directive to have the isolated scope directly attached to the controller, like this:
app.directive('xx', function () {
return {
bindToController: true,
controller: 'xxCtrl',
scope: {
label: '#',
},
};
});
Then in the controller I have a default in case label is not specified in the HTML:
app.controller('xxCtrl', function () {
var ctrl = this;
ctrl.label = ctrl.label || 'default value';
});
How can I instantiate xxCtrl in the Jasmine unit tests so I can test the ctrl.label?
describe('buttons.RemoveButtonCtrl', function () {
var ctrl;
beforeEach(inject(function ($controller) {
// What do I do here to set ctrl.label BEFORE the controller runs?
ctrl = $controller('xxCtrl');
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
Check this to test the issue
In Angular 1.3 (see below for 1.4+)
Digging into the AngularJS source code I found an undocumented third argument to the $controller service called later (see $controller source).
If true, $controller() returns a Function with a property instance on which you can set properties.
When you're ready to instantiate the controller, call the function and it'll instantiate the controller with the properties available in the constructor.
Your example would work like this:
describe('buttons.RemoveButtonCtrl', function () {
var ctrlFn, ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrlFn = $controller('xxCtrl', {
$scope: scope,
}, true);
}));
it('should have a label', function () {
ctrlFn.instance.label = 'foo'; // set the value
// create controller instance
ctrl = ctrlFn();
// test
expect(ctrl.label).toBe('foo');
});
});
Here's an updated Plunker (had to upgrade Angular to make it work, it's 1.3.0-rc.4 now): http://plnkr.co/edit/tnLIyzZHKqPO6Tekd804?p=preview
Note that it's probably not recommended to use it, to quote from the Angular source code:
Instantiate controller later: This machinery is used to create an
instance of the object before calling the controller's constructor
itself.
This allows properties to be added to the controller before the
constructor is invoked. Primarily, this is used for isolate scope
bindings in $compile.
This feature is not intended for use by applications, and is thus not
documented publicly.
However the lack of a mechanism to test controllers with bindToController: true made me use it nevertheless.. maybe the Angular guys should consider making that flag public.
Under the hood it uses a temporary constructor, we could also write it ourselves I guess.
The advantage to your solution is that the constructor isn't invoked twice, which could cause problems if the properties don't have default values as in your example.
Angular 1.4+ (Update 2015-12-06):
The Angular team has added direct support for this in version 1.4.0. (See #9425)
You can just pass an object to the $controller function:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
}, {
label: 'foo'
});
}));
it('should have a label', function () {
expect(ctrl.label).toBe('foo');
});
});
See also this blog post.
Unit Testing BindToController using ES6
If using ES6,you can import the controller directly and test without using angular mocks.
Directive:
import xxCtrl from './xxCtrl';
class xxDirective {
constructor() {
this.bindToController = true;
this.controller = xxCtrl;
this.scope = {
label: '#'
}
}
}
app.directive('xx', new xxDirective());
Controller:
class xxCtrl {
constructor() {
this.label = this.label || 'default value';
}
}
export default xxCtrl;
Controller Test:
import xxCtrl from '../xxCtrl';
describe('buttons.RemoveButtonCtrl', function () {
let ctrl;
beforeEach(() => {
xxCtrl.prototype.label = 'foo';
ctrl = new xxCtrl(stubScope);
});
it('should have a label', () => {
expect(ctrl.label).toBe('foo');
});
});
see this for more information:
Proper unit testing of
Angular JS
applications with ES6 modules
In my view, this controller is not meant to be tested in isolation, because it will never work in isolation:
app.controller('xxCtrl', function () {
var ctrl = this;
// where on earth ctrl.lable comes from???
ctrl.newLabel = ctrl.label || 'default value';
});
It is tightly coupled with the directive relying on receiving its scope properties. It is not re-usable. From looking at this controller, I have to wonder where this variable is coming from. It is no better than a leaky function internally using a variable from outside scope:
function Leaky () {
... many lines of code here ...
// if we are here we are too tired to notice the leakyVariable:
importantData = process(leakyVariable);
... mode code here ...
return unpredictableResult;
}
Now I have a leaky function whose behaviour is highly unpredictable based on the variable leakyVariable present (or not) in whatever scope the function is called.
Unsurprisingly this function is nightmare to test. Which is actually a good thing, perhaps to force the developer to rewrite the function into something more modular and re-usable. Which is not hard really:
function Modular (outsideVariable) {
... many lines of code here ...
// no need to hit our heads against the wall to wonder where the variable comes from:
importantData = process(outsideVariable);
... mode code here ...
return predictableResult;
}
No leaky issues and really easy to test and re-use. Which to me tells that using the good old $scope is a better way:
app.controller('xxCtrl', function ($scope) {
$scope.newLabel = $scope.label || 'default value';
});
Simple, short and easy to test. Plus no bulky directive object definition.
The original reasoning behind the controllerAs syntax was the leaky scope inherited from the parent. However, directive's isolated scope already solves this problem. Thus I don't see any reason to use the bulkier leaky syntax.
I've found a way that is not particulary elegant but works at least (if there's a better option leave a comment).
We set the value that "comes" from the directive, and then we call the controller function again to test whatever it does. I've made a helper "invokeController" to be more DRY.
For example:
describe('buttons.RemoveButtonCtrl', function () {
var ctrl, $scope;
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('xxCtrl', {
$scope: scope,
});
}));
it('should have a label', function () {
ctrl.label = 'foo'; // set the value
// call the controller again with all the injected dependencies
invokeController(ctrl, {
$scope: scope,
});
// test whatever you want
expect(ctrl.label).toBe('foo');
});
});
beforeEach(inject(function ($injector) {
window.invokeController = function (ctrl, locals) {
locals = locals || {};
$injector.invoke(ctrl.constructor, ctrl, locals);
};
}));
Is it possible to instantiate a controller in AngularJS and pass arguments to its constructor like in OOP ? I can't figure out how to refactor 3 identical controller with just variables name and content which change...
Thanx.
If you have 3 separate sections on the page that have very similar controller code, it sounds like you should consider using a directive. Even if you don't need to control the DOM directly (which is the classic reason to use directive), and only need the standard Angular data-bindings, then this is a nice way to reuse controllers in different contexts by the attributes set on the directive.
You can see a working plunkr at
http://plnkr.co/edit/qclp6MOxGWP7Ughod4T8?p=preview
But the key point is directives can bind-to variables in their parent scope's controller. Say, in the parent scope you have 3 variables, so:
$scope.myVariable1 = 'Value 1';
$scope.myVariable2 = 'Value 2';
$scope.myVariable3 = 'Value 3';
Then you can setup 3 instances of the directive in the template:
<my-directive my-param="myVariable1"></my-directive>
<my-directive my-param="myVariable2"></my-directive>
<my-directive my-param="myVariable3"></my-directive>
Then each directive can use the variable in the 'my-param' attribute
scope: {
'myParam':'='
}
The '=' means that in the scope of the directive you have a variable, called 'myParam', that is equal (+ bound to) the variable specified by the 'my-param' attribute on the directive. So on the template of the directive, you can use:
<div>Value of parameter: {{myParam}}</div>
And in the controller of the directive, you have access to is as:
$scope.myParam
And should then be able to customise its behaviour based on that instance's myParam.
You can create services with a common interface and then pass the corresponding services to each controller through dependency injection. In my code this is the case of table controllers where the only thing that changes is the data source:
Imagine you have some code that looks like this
angular.module('myapp').controller('TableACtrl', ['$scope', function($scope) {
$scope.data = // get the data from some source
$scope.someFunction = function () { ... };
$scope.someOtherFunction = function () { ... };
}]);
angular.module('myapp').controller('TableBCtrl', ['$scope', function($scope) {
$scope.data = // get the data from some other source
$scope.someFunction = function () { ... };
$scope.someOtherFunction = function () { ... };
}]);
It looks something like:
var tableCtrl = function($scope, dataSource) {
$scope.data = dataSource.getData();
$scope.someFunction = function () { ... };
$scope.someOtherFunction = function () { ... };
};
angular.module('myapp')
.controller('TableACtrl', ['$scope', 'dataSourceA', tableCtrl])
.controller('TableBCtrl', ['$scope', 'dataSourceB', tableCtrl])
angular.module('myapp')
.service('dataSourceA', function() {
return {
getData: function() { ... }
};
});
angular.module('myapp')
.service('dataSourceB', function() {
return {
getData: function() { ... }
};
});
I would still put things within a module, so you don't pollute the global window. But that's a different issue.