Test Driven Development – With Jasmine and Angular



Test Driven Development – With Jasmine and Angular

1 1


jasmine-tdd


On Github tygern / jasmine-tdd

Test Driven Development

With Jasmine and Angular

Tyson Gern @tygern

Introduction

  • Software Engineer at Pivotal Labs
  • 100% TDD and Pair Programming
  • We're new in town
  • We're hiring! (pivotaldublin.com)

Goals

  • You should be testing your javascript.
  • TDD results in clean, maintainable code.
  • Jasmine + AngularJS is a great way to start!

Tools

What is TDD?

According to Kent Beck...

Don't write a line of new code unless you first have a failing automated test. Eliminate duplication.

How to practice TDD?

Red Green Refactor

Write a short failing test

  • Watch your test fail

Make the test pass in the simplest way possible

  • Nothing too fancy
  • Duplicate code, if needed

Eliminate duplication introduced in the previous step

  • Improve code without changing behavior
  • Don't forget!

Why practice TDD?

Documents behavior of code

  • Easy for new developers to join project
  • Stays up to date, unlike comments
// adds one to the passed in value
function increment(number) {
  return number + 2;
}

Why practice TDD?

Leads to simple design

  • Tests are first client of code
  • Solution uses minimal amount of code

Why practice TDD?

Allows frequent refactoring

  • Code improves over time

Why practice TDD?

Quickly add features

  • Regressions are uncommon
  • Deploy confidently

Jasmine

javascript testing framework

  • Developed by Pivotal Labs
  • Framework independent
  • Runs anywhere

Example

Function addStrings that adds integers passed as strings.

Add a Test

describe('addStrings', function () {




});

Add a Test

describe('addStrings', function () {
  it('correctly adds two numbers', function () {


  });
});

Add a Test

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);

  });
});

Add a Test

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);
    expect(addStrings('4', '6')).toEqual(10);
  });
});

Add a Test

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);
    expect(addStrings('4', '6')).toEqual(10);
  });
});
ReferenceError: addStrings is not defined

Make it pass

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);
    expect(addStrings('4', '6')).toEqual(10);
  });
});
function addStrings() {

}

Make it pass

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);
    expect(addStrings('4', '6')).toEqual(10);
  });
});
function addStrings() {

}
Expected undefined to equal 8.

Make it pass

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);
    expect(addStrings('4', '6')).toEqual(10);
  });
});
function addStrings() {
  return 8;
}

Make it pass

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);
    expect(addStrings('4', '6')).toEqual(10);
  });
});
function addStrings() {
  return 8;
}
Expected 8 to equal 10.

Make it pass

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);
    expect(addStrings('4', '6')).toEqual(10);
  });
});
function addStrings(first, second) {
  return parseInt(first) + parseInt(second);
}

Make it pass

describe('addStrings', function () {
  it('correctly adds two numbers', function () {
    expect(addStrings('3', '5')).toEqual(8);
    expect(addStrings('4', '6')).toEqual(10);
  });
});
function addStrings(first, second) {
  return parseInt(first) + parseInt(second);
}

Requirement

Handle bad data. Count non-numbers as 0.

Add a test

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {


  });
});
function addStrings(first, second) {
  return parseInt(first) + parseInt(second);
}

Add a test

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);

  });
});
function addStrings(first, second) {
  return parseInt(first) + parseInt(second);
}

Add a test

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  return parseInt(first) + parseInt(second);
}

Add a test

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  return parseInt(first) + parseInt(second);
}
Expected NaN to equal 6.

Add a test

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  var firstNumber = parseInt(first);
  if(isNaN(firstNumber)) {
    firstNumber = 0;
  }

  return firstNumber + parseInt(second);
}

Add a test

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  var firstNumber = parseInt(first);
  if(isNaN(firstNumber)) {
    firstNumber = 0;
  }

  return firstNumber + parseInt(second);
}
Expected NaN to equal 2.

Add a test

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  var firstNumber = parseInt(first);
  if(isNaN(firstNumber)) {
    firstNumber = 0;
  }

  var secondNumber = parseInt(second);
  if(isNaN(secondNumber)) {
    secondNumber = 0;
  }

  return firstNumber + secondNumber;
}

Add a test

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  var firstNumber = parseInt(first);
  if(isNaN(firstNumber)) {
    firstNumber = 0;
  }

  var secondNumber = parseInt(second);
  if(isNaN(secondNumber)) {
    secondNumber = 0;
  }

  return firstNumber + secondNumber;
}

Refactor

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  var firstNumber = parseInt(first);
  if(isNaN(firstNumber)) {
    firstNumber = 0;
  }

  var secondNumber = parseInt(second);
  if(isNaN(secondNumber)) {
    secondNumber = 0;
  }

  return firstNumber + secondNumber;
}

Refactor

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  function parseNumber(string) {
    var number = parseInt(string);

    return isNaN(number) ? 0 : number;
  }

  return parseNumber(first) + parseNumber(second);
}

Refactor

describe('addStrings', function () {
  //...
  it('treats non-numbers as 0', function () {
    expect(addStrings('a', '6')).toEqual(6);
    expect(addStrings('2', '')).toEqual(2);
  });
});
function addStrings(first, second) {
  function parseNumber(string) {
    var number = parseInt(string);

    return isNaN(number) ? 0 : number;
  }

  return parseNumber(first) + parseNumber(second);
}

Angular

javascript web framework

  • Developed by Google
  • It's testable!

2-way binding with $scope

Angular synchronizes controllers and views with $scope.

<div ng-controller="messaging.flashController">
  <h3>{{message}}</h3>
</div>

+

angular.module('messaging')
  .controller('messaging.flashController', function ($scope) {
    $scope.message = 'hello';
  });

=

<div ng-controller="messaging.flashController">
  <h3>hello</h3>
</div>

Dependency injection

Angular uses dependency injection to provide objects with collaborators.

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             messagingService) {

  });

Modules

Modules allow building application with modules.

angular.module('users', []);            //   greeterApplication
angular.module('twitterAdapter', []);   //         /     \
angular.module('messaging', [           //        /       \
  'twitterAdapter'                      //       /         \
]);                                     //  messaging     users
                                        //      |
angular.module('greeterApplication', [  //      |
  'users',                              //      |
  'messaging'                           // twitterAdapter
]);                                     //
          

Example!

Start to build greeterApplication

Feature

Show initial greeting when the page loads

<div ng-app="greeterApplication">
  <div ng-controller="messaging.flashController">
    <h3>{{message}}</h3>
  </div>
</div>

Test setup

describe('messaging.flashController', function () {

  beforeEach(module('messaging'));












  // tests go here
});

Test setup

describe('messaging.flashController', function () {
  var $scope;
  beforeEach(module('messaging'));

  beforeEach(inject(function ($rootScope) {
    $scope = $rootScope.$new();







  }));

  // tests go here
});

Test setup

describe('messaging.flashController', function () {
  var $scope;
  beforeEach(module('messaging'));

  beforeEach(inject(function ($rootScope, $controller) {
    $scope = $rootScope.$new();



    $controller('messaging.flashController', {
      $scope: $scope

    });
  }));

  // tests go here
});

Test setup

describe('messaging.flashController', function () {
  var $scope, messagingService;
  beforeEach(module('messaging'));

  beforeEach(inject(function (..., _messagingService_) {
    $scope = $rootScope.$new();
    messagingService = _messagingService_;
    spyOn(messagingService, 'getMessage').and.returnValue('Hi!');

    $controller('messaging.flashController', {
      $scope: $scope,
      messagingService: messagingService
    });
  }));

  // tests go here
});

Add a Test

describe('messaging.flashController', function () {
  // setup

  describe('when the controller loads', function () {
    it('sets the message', function () {



    });
  });
});

Add a Test

describe('messaging.flashController', function () {
  // setup

  describe('when the controller loads', function () {
    it('sets the message', function () {
      expect($scope.message).toEqual('Hi!');


    });
  });
});

Add a Test

describe('messaging.flashController', function () {
  // setup

  describe('when the controller loads', function () {
    it('sets the message', function () {
      expect($scope.message).toEqual('Hi!');
      expect(messagingService.getMessage)
              .toHaveBeenCalledWith('initial');
    });
  });
});

Add a Test

describe('messaging.flashController', function () {
  // setup

  describe('when the controller loads', function () {
    it('sets the message', function () {
      expect($scope.message).toEqual('Hi!');
      expect(messagingService.getMessage)
              .toHaveBeenCalledWith('initial');
    });
  });
});
Expected undefined to equal 'Hi!'.

Add a Test

describe('messaging.flashController', function () {
  // setup

  describe('when the controller loads', function () {
    it('sets the message', function () {
      expect($scope.message).toEqual('Hi!');
      expect(messagingService.getMessage)
              .toHaveBeenCalledWith('initial');
    });
  });
});
Expected spy getMessage to have been called with [ 'initial' ] but it was never called.

Make it pass!

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             messagingService) {

  });

Make it pass!

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             messagingService) {
    $scope.message = messagingService.getMessage('initial');
  });

Make it pass!

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             messagingService) {
    $scope.message = messagingService.getMessage('initial');
  });

Refactor?

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             messagingService) {
    $scope.message = messagingService.getMessage('initial');
  });

Refactor?

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             messagingService) {
    $scope.message = messagingService.getMessage('initial');
  });

Feature

Change the message to prompt the user after 5 seconds

<div ng-app="greeterApplication">
  <div ng-controller="messaging.flashController">
    <h3>{{message}}</h3>
  </div>
</div>

Add a Test

describe('messaging.flashController', function () {
  // tests and setup

  describe('after waiting 5 seconds', function () {
    it('shows the \'prompt\' message', function () {










    });
  });
});

Add a Test

describe('messaging.flashController', function () {
  // tests and setup

  describe('after waiting 5 seconds', function () {
    it('shows the \'prompt\' message', function () {
      $scope.message = '';

      $timeout.flush(4900);
      expect($scope.message).toEqual('');






    });
  });
});

Add a Test

describe('messaging.flashController', function () {
  // tests and setup

  describe('after waiting 5 seconds', function () {
    it('shows the \'prompt\' message', function () {
      $scope.message = '';

      $timeout.flush(4900);
      expect($scope.message).toEqual('');

      $timeout.flush(200);

      expect($scope.message).toEqual('Hi!');
      expect(messagingService.getMessage)
              .toHaveBeenCalledWith('prompt');
    });
  });
});

Add a Test

describe('messaging.flashController', function () {
  // tests and setup

  describe('after waiting 5 seconds', function () {
    it('shows the \'prompt\' message', function () {
      $scope.message = '';

      $timeout.flush(4900);
      expect($scope.message).toEqual('');

      $timeout.flush(200);

      expect($scope.message).toEqual('Hi!');
      expect(messagingService.getMessage)
              .toHaveBeenCalledWith('prompt');
    });
  });
});
Expected '' to equal 'Hi!'.

Add a Test

describe('messaging.flashController', function () {
  // tests and setup

  describe('after waiting 5 seconds', function () {
    it('shows the \'prompt\' message', function () {
      $scope.message = '';

      $timeout.flush(4900);
      expect($scope.message).toEqual('');

      $timeout.flush(200);

      expect($scope.message).toEqual('Hi!');
      expect(messagingService.getMessage)
              .toHaveBeenCalledWith('prompt');
    });
  });
});
Expected spy getMessage to have been...

Make it pass!

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             $timeout,
                                             messagingService) {
    $scope.message = messagingService.getMessage('initial');




  });

Make it pass!

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             $timeout,
                                             messagingService) {
    $scope.message = messagingService.getMessage('initial');

    $timeout(function () {
      $scope.message = messagingService.getMessage('prompt');
    }, 5000);
  });

Make it pass!

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             $timeout,
                                             messagingService) {
    $scope.message = messagingService.getMessage('initial');

    $timeout(function () {
      $scope.message = messagingService.getMessage('prompt');
    }, 5000);
  });

Refactor?

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             $timeout,
                                             messagingService) {
    $scope.message = messagingService.getMessage('initial');

    $timeout(function () {
      $scope.message = messagingService.getMessage('prompt');
    }, 5000);
  });

Refactor!

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             $timeout,
                                             messagingService) {
    function setMessage(type) {
      $scope.message = messagingService.getMessage(type);
    }

    setMessage('initial');

    $timeout(function () {
      setMessage('prompt');
    }, 5000);
  });

Refactor!

angular.module('messaging')
  .controller('messaging.flashController', function($scope,
                                             $timeout,
                                             messagingService) {
    function setMessage(type) {
      $scope.message = messagingService.getMessage(type);
    }

    setMessage('initial');

    $timeout(function () {
      setMessage('prompt');
    }, 5000);
  });

Feature

Show the current user

<div ng-app="greeterApplication">
  <div ng-controller="users.currentController">
    <h2>Welcome {{currentUser}}!</h2>
  </div>

  <div ng-controller="messaging.flashController">
    <h3>{{message}}</h3>
  </div>
</div>

Setup

describe('users.currentController', function () {
  var $scope, usersService, deferred;
  beforeEach(module('users'));

  beforeEach(inject(function ($rootScope, $controller, $q,
                              _usersService_) {
    $scope = $rootScope.$new();
    usersService = _usersService_;





    $controller('users.currentController', {
      $scope: $scope,
      usersService: usersService
    });
  }));

  // tests go here
});

Setup

describe('users.currentController', function () {
  var $scope, usersService, deferred;
  beforeEach(module('users'));

  beforeEach(inject(function ($rootScope, $controller, $q,
                              _usersService_) {
    $scope = $rootScope.$new();
    usersService = _usersService_;

    deferred = $q.defer();



    $controller('users.currentController', {
      $scope: $scope,
      usersService: usersService
    });
  }));

  // tests go here
});

Setup

describe('users.currentController', function () {
  var $scope, usersService, deferred;
  beforeEach(module('users'));

  beforeEach(inject(function ($rootScope, $controller, $q,
                              _usersService_) {
    $scope = $rootScope.$new();
    usersService = _usersService_;

    deferred = $q.defer();
    spyOn(usersService, 'getCurrent')
      .and.returnValue(deferred.promise);

    $controller('users.currentController', {
      $scope: $scope,
      usersService: usersService
    });
  }));

  // tests go here
});

Add a Test

describe('when the controller loads', function () {
  it('sets the current user', function () {






  });
});

Add a Test

describe('when the controller loads', function () {
  it('sets the current user', function () {
    expect($scope.currentUser).toEqual('Loading');





  });
});

Add a Test

describe('when the controller loads', function () {
  it('sets the current user', function () {
    expect($scope.currentUser).toEqual('Loading');

    deferred.resolve('@walken20');



  });
});

Add a Test

describe('when the controller loads', function () {
  it('sets the current user', function () {
    expect($scope.currentUser).toEqual('Loading');

    deferred.resolve('@walken20');
    $scope.$apply();


  });
});

Add a Test

describe('when the controller loads', function () {
  it('sets the current user', function () {
    expect($scope.currentUser).toEqual('Loading');

    deferred.resolve('@walken20');
    $scope.$apply();

    expect($scope.currentUser).toEqual('@walken20');
  });
});

Add a Test

describe('when the controller loads', function () {
  it('sets the current user', function () {
    expect($scope.currentUser).toEqual('Loading');

    deferred.resolve('@walken20');
    $scope.$apply();

    expect($scope.currentUser).toEqual('@walken20');
  });
});
Expected undefined to equal 'Loading'.

Add a Test

describe('when the controller loads', function () {
  it('sets the current user', function () {
    expect($scope.currentUser).toEqual('Loading');

    deferred.resolve('@walken20');
    $scope.$apply();

    expect($scope.currentUser).toEqual('@walken20');
  });
});
Expected undefined to equal '@walken20'.

Make it Pass

angular.module('users')
  .controller('users.currentController', function($scope,
                                               usersService) {





  });

Make it Pass

angular.module('users')
  .controller('users.currentController', function($scope,
                                               usersService) {
    $scope.currentUser = 'Loading';




  });

Make it Pass

angular.module('users')
  .controller('users.currentController', function($scope,
                                               usersService) {
    $scope.currentUser = 'Loading';

    usersService.getCurrent().then(function (result) {
      $scope.currentUser = result;
    });
  });

Make it Pass

angular.module('users')
  .controller('users.currentController', function($scope,
                                               usersService) {
    $scope.currentUser = 'Loading';

    usersService.getCurrent().then(function (result) {
      $scope.currentUser = result;
    });
  });

Refactor?

angular.module('users')
  .controller('users.currentController', function($scope,
                                               usersService) {
    $scope.currentUser = 'Loading';

    usersService.getCurrent().then(function (result) {
      $scope.currentUser = result;
    });
  });

Refactor!

angular.module('users')
  .controller('users.currentController', function($scope,
                                               usersService) {
    function setCurrentUser(user) {
      $scope.currentUser = user;
    }

    setCurrentUser('Loading');

    usersService.getCurrent().then(setCurrentUser);
  });

Refactor!

angular.module('users')
  .controller('users.currentController', function($scope,
                                               usersService) {
    function setCurrentUser(user) {
      $scope.currentUser = user;
    }

    setCurrentUser('Loading');

    usersService.getCurrent().then(setCurrentUser);
  });
  • You should be testing your javascript.
  • TDD results in clean, maintainable code.
  • Jasmine + AngularJS is a great way to start!

Thanks!

Test Driven Development With Jasmine and Angular Tyson Gern @tygern