Thursday, June 27, 2013

Angular JS - Unit Testing - Directives

Testing directives is only slightly different than testing controllers or services, as I've covered in previous posts. Since you can inject services into directives, you can still mock those services by providing custom mocks as outlined in my "Unit Testing Services" post. But the major difference in that testing directives involves the DOM, which can make things difficult.

$compile is your friend.


To test a directive, you're going to need to compile a view featuring the directive, then probe DOM elements in that view to assert that they've been affected properly.

Basic Directive Example

This is a simple little directive that basically will change the text of an element to the evaluated value of an expression when the element is clicked.

app.directive('sampleOne', function (){
    // this is an attribute with no required controllers, 
    // and no isolated scope, so we're going to use all the
    // defaults, and just providing a linking function.
    
    return function(scope, elem, attrs) {
      elem.bind('click', function(){
        elem.text(scope.$eval(attrs.sampleOne));
      });
    };
});


Using $compile and angular.element() for testing


So to test this directive, we're going to create a string of html as a view that features the directive we want to test, then we're going to $compile that view against a scope we create. After that, we can alter the scope as we need to, and/or use jqLite (packaged with Angular) or JQuery (works well with Angular) to do some DOM manipulation and test some values.

IMPORTANT: Be sure to call scope.$digest() after you make changes to your scope and before you make your assertions!

Since Angular won't be doing this for you because you're testing, you have to be sure to call $digest() to update your view and model.

describe('Testing sampleOne directive', function() {
  var scope,
      elem,
      directive,
      compiled,
      html;
      
  beforeEach(function (){
    //load the module
    module('plunker');
    
    //set our view html.
    html = '<div sample-one="foo"></div>';
    
    inject(function($compile, $rootScope) {
      //create a scope (you could just use $rootScope, I suppose)
      scope = $rootScope.$new();
      
      //get the jqLite or jQuery element
      elem = angular.element(html);
      
      //compile the element into a function to 
      // process the view.
      compiled = $compile(elem);
      
      //run the compiled view.
      compiled(scope);
      
      //call digest on the scope!
      scope.$digest();
    });
  });

  it('Should set the text of the element to whatever was passed.', function() {
    //set a value (the same one we had in the html)
    scope.foo = 'bar';
    
    //check to see if it's blank first.
    expect(elem.text()).toBe('');
    
    //click the element.
    elem[0].click();
    
    //test to see if it was updated.
    expect(elem.text()).toBe('bar');
  });
});



UPDATE: I've added one more example to the Plunk, so have a look below.


And of course, here's a plunker demonstrating a basic directive test:
http://plnkr.co/edit/oTuRbYTPt8RyybUzk5ND?p=preview

Angular JS - Unit Testing - Services


Edit: (updated Feb 28, 2014)


I did a previous entry on unit testing controllers (it also covers the basic set up for Jasmine testing).That's probably the most important thing you can unit test, but what about services? Those are the piece you're using to do things like talk to other controllers, send AJAX requests, and/or interact with any dependency in your Angular application.

Testing services isn't exactly straight forward, at least if you're coming from the mindset of testing controllers. It can get a little more convoluted when your service has dependencies on other services. How can you test it in isolation from the other pieces?


Basic service testing


To start off, let's look at an extremely basic service. Something that just does something really minor, but you use this functionality everywhere, so you'd like it to be injectable.  Below you'll see a really simple service that just manipulates some text and returns it.


To test this, what we need to do is simply inject it with angular-mocks inject() helper function in our beforeEach() call, then we can make calls to it and test the outcomes:


Testing a service with $http using $httpBackend


Things get a little trickier when we want to test a service that depends on other services, particularly in the case of $http. Fortunately, $http relies on $httpBackend in Angular, and angular-mocks.js automatically provides a mock $httpBackend for you.

So if we were to look at a simple service that gets data over $http, it might look something like this:


The process here is fairly similar to what we did above. Since angular-mocks.js has already provided the mock $httpBackend, we can use some functions it's added to test how $http is being used, and even to mock what it returns. We simply need to inject the $httpBackend and make the appropriate calls to it's newly provided test helper functions, such as when(), expectGET() or expectPOST(). We also need to add an afterEach() call to assert that our expectations set in $httpBackend were met.:


Providing custom mocks


Finally, you're likely to have the need to provide mocks to other services in order to test your service in isolation. For example, if you have a service foo that depends on service bar, you may want to inject a mock to bar so you can test foo in isolation from bar. So presuming we have the following service, and we want to test myService in isolation from foo:


Providing mocks is as easy as adding an anonymous function to the module() call that loads your module, that set up values in it's provider. At the same point in your beforeEach() that you're loading the module you're testing, just add a second parameter that is a function that sets up your mock (replacing whatever service was there before).



That should be all you require to test your services.  Test well, test thoroughly, test red-green if you can. If you're testing something that injects $http, use $httpBackend where you can, but you can, of course, mock $http yourself if you wanted to. The world is your oyster here, as long as your tests cover your code well.


And, since I try to provide something for people to play with, here is a working plunk demonstrating the techniques I wrote about above.

Monday, June 3, 2013

Pittsburgh TechFest 2013 - AngularJS Single Page Application Development

I've put the source code for the project for my presentation up on GitHub. It's available here:


I had a blast at TechFest, and I'm really happy with the level of interest in my talk. Thank you very much to all of those who attended my talk, and thank you also to the other presenters for sharing their amazing knowledge on a wide variety of tech topics. I thoroughly enjoyed this TechFest and I look forward to next year's.