TDD in JavaScript using the example of AngularJS project

 

Andrey Kucherenko

EPAM Systems

TDD steps

  • Navigation map

  • Test first

  • Assert first

  • Fail first

  • Continious integration

Baby steps 

Pair programming

Emergent Design

Continious integration

Angular JS

MVW? framework

Two way binding
HTML templates
$scope

Lineman.js

Start AngularJS project with lineman

  • git clone https://github.com/linemanjs/lineman-angular-template.git lineman-app
  • cd lineman-app
  • sudo npm install -g lineman
  • npm install
  • lineman run
$ lineman run
Running "common" task

Running "ngtemplates:app" (ngtemplates) task
File generated/angular/template-cache.js created.

Running "less:compile" (less) task
File generated/css/app.less.css created.

Running "jshint:files" (jshint) task
>> 9 files lint free.

Running "concat_sourcemap:js" (concat_sourcemap) task
File "generated/js/app.js" created.

Running "concat_sourcemap:spec" (concat_sourcemap) task
File "generated/js/spec.js" created.

Running "concat_sourcemap:css" (concat_sourcemap) task
File "generated/css/app.css" created.

Running "images:dev" (images) task
Copying images to 'generated/img'

Running "webfonts:dev" (webfonts) task
Copying webfonts to 'generated/webfonts'

Running "pages:dev" (pages) task
generated/index.html generated from app/pages/index.us

Running "dev" task

Running "server" task
Starting express web server in 'generated' on port 8000
Simulating HTML5 pushState: Serving up 'generated/index.html' for all other unmatched paths

Running "watch" task
Waiting...
$lineman spec


TEST'EM 'SCRIPTS!                                                               
Open the URL below in a browser to connect.                                     
http://localhost:7357/                                                          
               ┏━━━━━━━━━━━━━┓                                                  
 PhantomJS 1.9 ┃ Chrome 34.0 ┃                                                  
    9/9 ✔      ┃   9/9 ✔     ┃                                                  
━━━━━━━━━━━━━━━┛             ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✔ 9 tests complete.















[Press ENTER to run tests; q to quit]  

Testing with Jasmine and AngularJS

// The inject function wraps 
// a function into an injectable function.
beforeEach(inject(function ($rootScope) {}));


// This function registers 
//a module configuration code
beforeEach( module(function ($provide) {});

// $controller service is responsible for 
// instantiating controllers.
sut = $controller('MyController', {});

Tests for Controllers

describe('Controller: Sandbox', function () {

    // load the controller's module
    beforeEach(module('agameApp'));

    var MainCtrl, scope, element;

    // Initialize the controller and a mock scope
    beforeEach(inject(function ($controller, $rootScope) {
        element = {
            isOpened: false
        };
        scope = $rootScope.$new();
        MainCtrl = $controller('Sandbox', {
            $scope: scope
        });
    }));

    it('should open after click', function () {
        scope.show(element);
        expect(element.isOpened).toEqual(true);
    });

});

Tests for Directives

describe("directive: shows-message-when-hovered", function() {

  beforeEach(function() {
    module("app");
  });

  beforeEach(inject(function($rootScope, $compile) {
    this.directiveMessage = 'ralph was here';
    this.html = "<div shows-message-when-hovered message='" 
                  + this.directiveMessage + 
                "'></div>";
    this.scope = $rootScope.$new();
    this.scope.message = 
    this.originalMessage = 
                    'things are looking grim';
    this.elem = $compile(this.html)(this.scope);
  }));

 describe("when a users mouse leaves the element", function() {
    it("restores the message to the original", function() {
      this.elem.triggerHandler('mouseleave');
      expect(this.scope.message).toBe(this.originalMessage);
    });
  });

});

Tests with deferred

/** test for code like this
MyResource.call().then(function () { ...code... });
**/
beforeEach(inject(function ($q, $controller, $rootScope) {
    
    mockResource.call = jasmine.createSpy().andCallFake(function () {
        deferred = $q.defer();
        deferred.resolve({test: 'data'});
        return deferred.promise;
    });

    scope = $rootScope.$new();

    $controller('MyController', {
            $scope: scope
            MyResource: mockResource
    });
    scope.$apply();
}));

Configuration Module

describe("ConfigService", function() {  
  var sut, rootScope, mockLocalStorageService, 
        mockSetDeviceLocationService, expectStore, 
        mockWindow, mockLocation;

  beforeEach(function() {
    module("common");
    
    module(function ($provide) {
        $provide.value('LocalStorageService', 
                        mockLocalStorageService);
        $provide.value('SetDeviceLocationService', 
                        mockSetDeviceLocationService); 
        $provide.value('$window', mockWindow);
        $provide.value('$location', mockLocation);
    });
  });  
  
  beforeEach(inject(function(ConfigService, $rootScope) {
      sut = ConfigService;
      rootScope = $rootScope;
  }));
  
  describe("Current Store", function(){
      it("will load the current store from localstorage", function() {   
          sut.initialize();
          expect(mockLocalStorageService.getCurrentStore).toHaveBeenCalled();
      }); 
      
      it("will load the current store to the rootscope", function() {   
          sut.initialize();
          expect(rootScope.currentStore).toEqual(expectStore);
      });             
  }); 

});

Thank you! Questions?

 

Navigation map

Test first

describe('Calculator', 
    function () {
        it('should summarize two numbers', 
            function () {
        
            }
        );
    }
);

Assert first

describe('Calculator', function () {

    it('should summarize two numbers', 
        function () {
            sut.sum(1, 2).should.equal(3);
        }
    );

});

Fail first

> mocha test/*.spec.js


  ․

  0 passing (4ms)
  1 failing

  1) String Calculator "before each" hook:
     ReferenceError: Calculator is not defined
      at Context.<anonymous> (/home/apk/workspace/lab/tdd/test/Calculator.spec.js:5:15)
      at callFn (/home/apk/workspace/lab/tdd/node_modules/mocha/lib/runnable.js:223:21)
      at Hook.Runnable.run (/home/apk/workspace/lab/tdd/node_modules/mocha/lib/runnable.js:216:7)
      at next (/home/apk/workspace/lab/tdd/node_modules/mocha/lib/runner.js:258:10)
      at Object._onImmediate (/home/apk/workspace/lab/tdd/node_modules/mocha/lib/runner.js:275:5)
      at processImmediate [as _immediateCallback] (timers.js:330:15)

Continious integration

TDD for javascript

By Andrey Kucherenko

TDD for javascript

TDD in JavaScript using the example of AngularJS project

  • 2,116