Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

bindToController in unit tests

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

like image 555
ernestoalejo Avatar asked Sep 14 '14 20:09

ernestoalejo


People also ask

What should be included in a unit test?

Unit tests should validate all of the details, the corner cases and boundary conditions, etc. Component, integration, UI, and functional tests should be used more sparingly, to validate the behavior of the APIs or application as a whole.

Can unit tests depend on each other?

Tests should never depend on each other. If your tests have to be run in a specific order, then you need to change your tests. Instead, you should make proper use of the Setup and TearDown features of your unit-testing framework to ensure each test is ready to run individually.

What is the meaning of unit test 1?

UNIT TESTING is a type of software testing where individual units or components of a software are tested. The purpose is to validate that each unit of the software code performs as expected. Unit Testing is done during the development (coding phase) of an application by the developers.


2 Answers

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.

like image 142
meyertee Avatar answered Oct 02 '22 14:10

meyertee


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

like image 40
svp Avatar answered Oct 02 '22 14:10

svp