TL;DR: We can test components as we were used to test directives.
Yes, you heard all-right, there is a new kid in town and last Friday Angular 1.5 was released bringing us, among other updates, the component sugar. If you want to know more about it Todd Motto explored the component method back in November when it was still beta.
Why components? The web is shifting towards web components and on the way we are adopting component based architectures. Angular 2 will be all about components and to ease the migration process we better start preparing now. If you haven’t started yet I recommend you to read thoroughly Refactoring Angular Apps to Component Style by Tero Parviainen and Preparing for Angular 2 by Juri Strumpflohner.
Unit testing components
The good news are that we can test components as we were used to test directives, because component is nothing but a helper method to create directives with some sensible defaults. Therefore if we are just switching the syntax from directive to component all our tests should keep passing.
The big difference with directives is that now we pass a definition object rather than a function returning an object. So our component can be as simple as follows.
angular.module('myComponentModule', []) .component('myComponent', { bindings: { myBinding: '@' }, controller: function() { this.myTitle = 'Unit Testing AngularJS'; }, template: ' <h1>{{ $ctrl.myTitle }} {{ $ctrl.myBinding }}</h1> ' });
By default the component controller is named $ctrl
and the bindings are passed from the parent scope to our component:
<my-component my-binding="1.5"></my-component>
So we can compile the element and test that it renders what we expected.
describe('Component: myComponent', function () { beforeEach(module('myComponentModule')); var element; var scope; beforeEach(inject(function($rootScope, $compile){ scope = $rootScope.$new(); element = angular.element('<my-component my-binding="{{outside}}"></my-component>'); element = $compile(element)(scope); scope.outside = '1.5'; scope.$apply(); })); it('should render the text', function() { var h1 = element.find('h1'); expect(h1.text()).toBe('Unit Testing AngularJS 1.5'); }); });
Testing the component controller
To test an instance of the component controller we can grab it from the compiled element.
var controller; beforeEach(function() { controller = element.controller('myComponent'); });
And test what the controller is exposing to the view.
it('should expose my title', function() { expect(controller.myTitle).toBeDefined(); expect(controller.myTitle).toBe('Unit Testing AngularJS'); });
That also includes the external bindings.
it('should have my binding bound', function() { expect(controller.myBinding).toBeDefined(); expect(controller.myBinding).toBe('1.5'); });
There is though a new addition for Angular 1.5 and the ngMock
module has been updated with the $componentController
method.
This help us to test the component controller when we don’t want or need to test the compiled element. So we can create an instance of the controller by just passing the scope and the bindings.
var controller; var scope; beforeEach(inject(function($rootScope, $componentController){ scope = $rootScope.$new(); controller = $componentController('myComponent', {$scope: scope}, {myBinding: '1.5'}); })); it('should expose my title', function() { expect(controller.myTitle).toBeDefined(); expect(controller.myTitle).toBe('Unit Testing AngularJS'); }); it('should have my binding bound', function() { expect(controller.myBinding).toBeDefined(); expect(controller.myBinding).toBe('1.5'); });
Patterns for unit testing
Not long ago I released a repository where I shared some of the common patterns that I’ve encountered while testing AngularJS applications. Now I’ve updated the example project with a component being tested as described in this post.
And although it’s early to start talking about patterns I guess the focus will be on testing the bindings and how the actions related to them affect the outside and the inside of our components. Even more now that we have one-way data binding.
Last week I joined the Bootcamp of my friends at Philos.io as a mentor and I created a small app where I started digging a bit on some of these patterns for my dumb components.
Great. We are using angular in our project and these tips helps
Glad you liked it!
Nice, but if you use “templateUrl” instead of “template”, Angular security blocks the execution of “$compile(…)”. Angular considers the path in “templateUrl” as a cross domain request.
Hi Jurgen!
Using
templateUrl
you need to configure a Karma preprocessor to put the HTML into the$templateCache
. I explain it here.Cheers, Pablo.
Also make sure you $digest/$apply before trying to get the controller:
How do you test a component that uses a separately defined controller:
I’ve tried to use $componentController but it errors out with: Unknown provider: VolunteersComponentControllerDirectiveProvider <- VolunteersComponentControllerDirective
Hi Rob!
If you have registered the controller using the controller function as
angular.module('webApp', []).controller('VolunteersComponentController', function() {});
then you can use the$controller
method when testing the controller individually.Cheers, Pablo.
Pablo,
This technique looks really awesome! However, I am having trouble in my use case.
My problem is that the component under test references another component in its HTML template:
When I am testing A, I’m getting a failure because I didn’t mock all of BController’s dependencies.
What I’d like to do, ideally, is to create some dummy B component:
{
template: ‘Mock B’
}
…and test the interactions. How can I configure the AngularJS test framework to load this mock B component when compiling and linking the A component?
I’m messing around with angular.mock.module, but I’m having trouble getting it to work.
Any hint would be appreciated. Thanks!
Hi Rusell!
You can have a look to this Stack Overflow question and all the answers.
Basically you could replace your child component with a terminal mock directive using $compileProvider.
Cheers, Pablo.
What If I need to inject a service into the constructor of the component? Does that matter. Thanks for the code.
Hi Michael!
You could mock the service for testing the component in isolation.
Cheers, Pablo.
Thanks for the helpful article!
I’m having a little trouble with testing component controllers that include a callback, as when the app is running angular wraps the callback, however this doesn’t happen when the test instantiates the controller. This leads me to having to ‘unwrap’ any parameters in my test, which feels a bit strange. For example, if I have a component with an updateItems callback
bindings {
updateItems: ‘&’
}
and some controller code
function doSomething(){
self.updateItems({
items: itemsToPassToCallback
});
}
then in my test the callback function will receive an object with the parameters, but in production there is some ‘magic’ angular code which will map the object to the function parameters. I was curious if you had any opinion on the best approach to take here.
Hi Graham!
I’m not sure about what you mean with “angular wraps the callback”. Could you maybe post a JSFiddle, CodePen or Gist with more code?
In any case, I would test the
updateItems
function in the parent component which is the one defining the functionality.Cheers, Pablo.
Thanks for the response, I’ve added a fiddle – https://jsfiddle.net/xwaov2eh/9/
If you click the button then ‘text to pass to callback’ is logged in the console, however if you were to run the spec, you would see Object{text: ‘text to pass to callback’} logged, because the controller passes an object to the callback function.
When angular binds the callback method to the controller it wraps it in another function, which is why you need to pass an object with param name -> param value pairs, however this makes the test look a bit funny.
This isn’t a huge deal, I was just interested to get someone else’s opinion!
Hi Graham!
Sorry for the late reply but I’m in the middle of my holidays and finally got some time.
I’m not sure how are you testing this but I would create an spy and test that it has been called.
Cheers, Pablo.
Hi Pablo,
I’ve a parent app component that just has a ui-view directive, with no bindings or controller. When I create a mocked controller using $componentController, for some reason I get ‘Cannot export controller ” as ‘$ctrl’! No $scope object provided via `locals`.’ It works fine if I supply a scope from getting a new on rootscope. I have another test in different project that also has a similar component and the same test for it works fine! I don’t see any difference between both the tests though!! Are we supposed to pass a scope for app like root component that just has a ui-view directive and no bindings/controller?
Sid
Hi Sid!
I thought you always need to provide a
$scope
when using$componentController
. Do you mean that in your second test no scope is provided but the test still pass?Cheers, Pablo.
Hey Pablo, thank you, great post!
> expect(h1.text()).toBe(‘Unit Testing AngularJS 1.5’);
In which context does `.text()` work?
In a jQuery context (http://jqapi.com/#p=text) or in general?
It wasn’t working for me so I switched to `.innerText` (https://developer.mozilla.org/en-US/docs/Web/API/Node/innerText).
Just wanted to share this in case I just missed something or someone else runs into this.
Hi, Hannes!
text()
is part of the jqLite API so it should work without the need of including jQuery. What are the errors or problems you had encountered?Cheers, Pablo.
Ah right. It was me not knowing the limitations of `.find()` in the first plase. It says in angular docs: “find() – Limited to lookups by tag name”.
I still tried it, but then switched to use the dom element (element[0]) and so lost the jqLite functions. It was the wrong approach in the first place.
Thank you, your post and example project are very helpful!
Cool! You could still wrap that with
angular.element(element[0])
if you need the jQuery methods.Thank you and stay tuned because I’m planning to update that repo to celebrate the 100 stars.
Cheers, Pablo.
i may do something wrong but for me it seems that `$onChanges` isn’t called when outside values are changed with your method 😦
Without seeing your code it’s hard to guess, but make sure you trigger the digest cycle manually.
I followed exactly what you said about $componentController.
I am getting this error:
Error: [$injector:unpr] Unknown provider: $componentControllerProvider <- $componentController
http://errors.angularjs.org/1.5.11/$injector/unpr?p0=%24componentControllerProvider%20%3C-%20%24componentController (line 4563)
Hi, sonu! Are you including angular-mocks.js? Cheers.
Hi Pablo.
I am not sure of that. I don’t see any file with that name in my project directory. How do it include it ?
Yes I do have angular.mock file. I have it installed under devDependencies with version 1.5.8.
i updated the version of angular.mock and now i don’t see the error. Thanks for the help 🙂
I’m happy it worked for you!
Pingback: Unit test ngRedux components? (Inject a real service in an angular mock object?) – Angular Questions
Pingback: Unit test ngRedux components? (Inject a real service in an angular mock object?) – program faq
Hi Pablo,
I know this article is getting old, but it still saved me a lot of time today. Thanks for that.
I have a question about testing component controller when lifecycle hooks like $onInit are defined. What is the best way to deal with these hooks ? When using $componentController to create controller, $onInit is not triggered. Would you call explicitly $onInit method or rather create the entire component with $compile and use underlying controller ?
Thanks in advance
Hi, Sergio! I would stick with the $compile method, but I find ok to explicitly call the $onInit method when needed. Cheers, Pablo.
Hi Pablo.
I followed exactly what you said about $componentController.
I am getting this error:
Error: [$injector:unpr] Unknown provider: myComponentDirectiveProvider <- myComponentDirective
http://errors.angularjs.org/1.5.10/$injector/unpr?p0=myComponentDirectiveProvider%20%3C-%20myComponentDirective (line 4559)
Hi, Tanuj! Without having a look to the code it’s hard to say. Are you registering a directive or a component? Cheers, Pablo.
Pingback: How to Test $scope.$on in an AngularJS 1.5+ Component Using Jasmine – Angular Questions