Unit Tests in Angularjs – just do it



Unit Tests in Angularjs – just do it

0 0


unit-tests-angularjs-presentation


On Github Konviser / unit-tests-angularjs-presentation

Unit Tests in Angularjs

just do it

Svetka Sapelova/@SvetkaSapelova

About me

I am a front-end developer at Cirqle.nl

I am a member of CodeCatz

I tweet at @SvetkaSapelova

I get emails to svetsapelova@gmail.com

I come from Saint-Petersburg

I live in Ljubljana

Why Unit Tests?

..because we want to be sure our code works

..and because unit testing in Angularjs is pure fun

Oh hey, these are some notes. They'll be hidden in your presentation, but you can see them if you open the speaker notes window (hit 's' on your keyboard).

What to test?

Test the logic behind the components

Unit tests are all about mocks, stubs, spies, fakes, isolating …you name it

Angularjs is all about dependency injection


   while(Dependency Injection) {

      Angularjs.easyToMock = true;

   }


What is DI?

Dependency Injection..

...is a way to wire components of the app together

...enables loose coupling between the components

...is handled by Angularjs $injector service

Unit tests rely on the $injector to get all the components and ngMock module to mock angularjs services

Testing controllers

 
 'use strict';
 
 angular.module('jsmeetupApp')
   .controller('MainCtrl', function ($scope, DataService) {

     
   //Chain the promise

     DataService
       .getData()      
       .then(function(response){

        $scope.items = response.data;
       
       },
       function(err){

         $scope.loadDataFailed = true;
       
       });

   });

The following tests are made with Jasmine jasmine.github.io

How to mock promises

  
  beforeEach(inject(function ($controller, $rootScope, $q) {

    scope = $rootScope.$new();

    //create mock of the service
    MockDataService = {
      getData: function(){
      	defer = $q.defer();
        return defer.promise
      }
    };

   //initialize controller with mocked service
    MainCtrl = $controller('MainCtrl', {
      $scope: scope,
      DataService: MockDataService
    });
 });

  • Reject/resolve the promise
  • Kick the angular digest loop to execute callbacks
  • Check the results
  it('should attach a list of data to the scope', function () {

    //resolve the promise
    defer.resolve({data:['AngularJS', 'Emberjs', 'Backbone']});

    //kick the angular digest loop to propagate promise resolution
    scope.$apply();

    expect(scope.items.length).toEqual(3);

  });

  it('should set loadDataFailed to true', function () {

    //reject the promise
    defer.reject();
    scope.$apply();
    expect(scope.loadDataFailed).toBeTruthy();
  });

Rule of thumb

Don’t overload controllers with logic.

Slim controllers = testable controllers

Mocking dependencies

It's possible to override Angularjs components by creating the new ones with the same name

Angularjs $provide service registers components of the app We can make a use of it to override components we want to mock
 beforeEach(module('jsmeetupApp', function($provide){

  //this will override the original service
  $provide.service('DataService',function() {

      this.getData = function(){
        defer = $q.defer();
        return defer.promise
      };

    })

  }));

  beforeEach(inject(function ($controller, $rootScope, _$q_) 
  {

    scope = $rootScope.$new();
    $q = _$q_;

    MainCtrl = $controller('MainCtrl', {$scope: scope});

  }));

Use $provide to mock the dependecies for your app service. With controllers you can do both ways.

Ajax calls

A bit of theory

  • $http service performs Ajax requests
  • Behind the scene it accepts $httpbackend service as a dependency that delegates to XMLHttpRequest object or JSONP
  • ngMock has a mock implementation of $httpbackend
  • that intercepts all the requests and synchronously returns responses

OK, enough of theory

Ajax request

 'use strict';

 angular.module('jsmeetupApp')
    .service('DataService', function($http){

    this.getData = function(callback) {

      $http.get('api/getdata')
      .success(function(data){
        
        callback(null,data)
      })
      .error(function(err){

          callback(err,null)
      })
  
    };

});

 
 /*in beforeEach inject $httpBackend  */
 /* and $injector */

 $httpBackend.expectGET('api/getdata')
    .respond(200,['Angular']); 

 DataService = $injector.get('DataService');
 callback = jasmine.createSpy('callback');            
        ....

 it('should call the callback', function(){

   DataService.getData(callback);

   //return trained response
   $httpBackend.flush();

    expect(callback)
     .toHaveBeenCalledWith(null,['Angular']);
 
 });

* We are not talking about angularjs services here. You can find more information here
Similar to $httpBackend ngMock mocks $timeout, $interval etc services *docs.angularjs.org/api/ngMock

We are almost there

Directives

Directives are functions that bind specific behavour to the DOM element

*https://docs.angularjs.org/guide/directive

Directives are awesome because

they are reusable

Simple directive

 
  angular.module('Dirs')
  .directive('clicked', function(){

    return {
        link: function(scope,elem, attrs){

            elem.bind('click', function(){

                scope.clicked = true;
                scope.$apply()
                elem.hide();

            });
        },
        template: '<button>Click Me</button>'
    }
 });

  <div clicked></div>

Testing directives

make use of Angularjs $compile service

    
 beforeEach(inject(function(_$compile_,$rootScope){

  $compile = _$compile_;
  scope = $rootScope.$new();

   //that is how Angularjs turns html into view

  element = $compile('<div clicked></div>')(scope);
  scope.$digest();

 }));

$compile => $link => $digest

As long as we have ref to the html element, we can test things on it

 
 it('should display the button', function(){

   //verify the template has been loaded
   expect(element.find('button').length).toEqual(1);

 });

 it('should add display:none style', function(){

        element.triggerHandler('click');

        //we can chech the attrs of the element
        expect(element.css('display')).toEqual('none');
        
        //we can get access to the scope of the element
        expect(element.scope().clicked).toBeTruthy();

   });

Let's stop here

Tools

Karma (test runner for Angulajs) karma-runner.github.io Jasmine (javascript test framework) jasmine.github.io

Installation party

Install Karmanpm install Install karma command line npm install -g karma-cli Install Jasmine plugin for karma npm install karma-jasmine Install browser plugins for Karma (ex. Chrome)npm install karma-chrome-launcher Configure Karmakarma.conf.js Write tests Start Karma and see test resultskarma start karma.conf.js

There are alternatives

Testem (test runner ) github.com/airportyh/testem Mocha (javascript test framework) github.com/mochajs/mocha QUnit (javascript test framework) qunitjs.com

* zachlendon.github.io/blog/2013/03/26/quick-thoughts-on-testem-vs-testacular-karma techtalkdc.com/which-javascript-test-library-should-you-use-qunit-vs-jasmine-vs-mocha

Where to learn more

ng-book.comng-newsletterdocs.angularjs.org/guide/unit-testinglinkis.com/smashingmagazine.com/kG2hs yearofmoo.com/2013/01/full-spectrum-testing-with-angularjs-and-karma.htmls

Thank you!