Frontend Unit Testing

Why ?

Because you're not Chuck Norris. Chuck Norris' code tests itself, and it always passes, with 0ms execution time.

Pros

Increase your code modularity

Never forget dependencies

Ensure you logic is working

Faster than e2e tests

First step to TDD

Cons

Too much frameworks !

How ?

Test runner

BDD framework

Example n°1

Controller

angular.module 'pepiniere-dashboard-sirf.admin'
.controller 'AdminSignsCtrl', ($log, $scope, Sign, signs) ->
  $scope.signs = signs

  removed = []

  refresh = (list) ->
    list.forEach (item, i) ->
      item.position = i

  $scope.add = (index) ->
    $scope.signs.splice index, 0, new Sign()
    refresh $scope.signs

  ...

  $scope.doUpdate = (form) ->
    for item in removed
      item.$delete()
    for item in $scope.signs
      item.$save()

  return
describe 'AdminSignsCtrl', ->

  $controller = null
  $rootScope  = null
  $state      = null

  Sign        = null

  beforeEach module 'pepiniere-dashboard-sirf.admin'

  beforeEach inject (_$controller_, _$rootScope_, _$state_) ->
    $controller = _$controller_
    $rootScope  = _$rootScope_
    $state      = _$state_

  beforeEach inject (_Sign_) ->
    Sign = _Sign_

  describe 'router', ->

    state   = 'admin.signs'

    it 'should redirect to url', ->
      expect($state.href state).toEqual '#/admin/signs'

    it 'should resolve data', ->
      Sign.find = jasmine.createSpy('find').and.returnValue $promise: []
      $state.go state
      $rootScope.$digest()
      expect(Sign.find).toHaveBeenCalled()
      expect($state.current.name).toBe state
  describe '$scope.signs', ->

    it 'should be automatically set if not passed to controller', ->
      $scope = {}
      controller = $controller 'AdminSignsCtrl',
        $scope: $scope
        Sign: Sign
        signs: []
      expect(Array.isArray $scope.signs).toEqual true
      expect($scope.signs.length).toEqual 2
      for i, sign of $scope.signs
        expect(sign.position).toEqual parseInt i

    it 'should be set if passed to controller', ->
      $scope = {}
      controller = $controller 'AdminSignsCtrl',
        $scope: $scope
        Sign: Sign
        signs: [
          {
            position: 0
          }
          {
            position: 1
          }
          {
            position: 2
          }
        ]
      expect(Array.isArray $scope.signs).toEqual true
      expect($scope.signs.length).toEqual 3
  describe '$scope.{up,down,add,remove}', ->

    $scope = {}
    controller = null

    beforeEach ->
      $scope = {}
      controller = $controller 'AdminSignsCtrl',
        $scope: $scope
        Sign: Sign
        signs: [
          {position: 0, name: 'position 0', $save: jasmine.createSpy('$save')}
          {position: 1, name: 'position 1', $save: jasmine.createSpy('$save')}
          {position: 2, name: 'position 2', $save: jasmine.createSpy('$save')}
        ]

    it 'should add an sign', ->
      $scope.add 1
      expect(Array.isArray $scope.signs).toEqual true
      expect($scope.signs.length).toEqual 4

    it 'should save items on update', ->
      $scope.add 1
      $scope.signs[1].$save = jasmine.createSpy('$save')
      $scope.doUpdate {}
      expect($scope.signs[1].$save).toHaveBeenCalled()
      
    it 'should delete items on update', ->
      removedItem = $scope.signs[1]
      removedItem.$delete = jasmine.createSpy('$delete')
      $scope.remove 1
      $scope.doUpdate {}
      expect(removedItem.$delete).toHaveBeenCalled()

Example n°2

Directive

angular.module 'pepiniere-dashboard-sirf.comment'
.directive 'commentPanel', ->
  restrict: 'E'
  scope:
    comment: '='
    editable: '='
  templateUrl: 'comment/directives/comment-panel/template.html'
  link: ($scope) ->

    $scope.isCollapsed = true
    $scope.editing = false

    $scope.switchCollapse = ->
      $scope.isCollapsed = not $scope.isCollapsed

    $scope.switchEdit = ->
      $scope.editing = not $scope.editing

    $scope.save = ->
      $scope.comment.$save().then ->
        $scope.editing = false
describe '[DIRECTIVE] commentPanel', ->

  $compile    = null
  $q          = null
  $scope      = null
  $state      = null
  $rootScope  = null

  element     = null
  template    = '<comment-panel comment="comment" editable="editable"></comment-panel>'

  beforeEach ->
    angular.module 'textAngular', []
  beforeEach module 'pepiniere-dashboard-sirf.comment'

  beforeEach inject (_$compile_, _$q_, _$rootScope_, _$injector_, _$state_) ->
    $compile    = _$compile_
    $q          = _$q_
    $rootScope  = _$rootScope_
    $injector   = _$injector_
    $state      = _$state_

  beforeEach ->
    $scope = $rootScope.$new()
    def = $q.defer()
    def.resolve {}
    $scope.comment =
      content: ''
      $save: jasmine.createSpy('save').and.returnValue def.promise
    $scope.editable = true
    element = $compile(template) $scope
    $rootScope.$digest()
  it 'should inject template', ->
    expect(element.html()).toContain 'class="panel"'

  it 'should create child scope', ->
    $child = $scope.$$childTail
    expect($child).toBeDefined()
    expect($child.isCollapsed).toBe true
    expect($child.editing).toBe false
    expect($child.switchCollapse).toBeDefined()
    expect($child.switchEdit).toBeDefined()
    expect($child.save).toBeDefined()

  it 'should switch collapse', ->
    $child = $scope.$$childTail
    expect($child.isCollapsed).toBe true
    $child.switchCollapse()
    expect($child.isCollapsed).toBe false

  it 'should switch edition', ->
    $child = $scope.$$childTail
    expect($child.editing).toBe false
    $child.switchEdit()
    expect($child.editing).toBe true

  it 'should save', ->
    $child = $scope.$$childTail
    $child.switchEdit()
    expect($child.editing).toBe true
    $child.save()
    $rootScope.$digest()
    expect($child.editing).toBe false
    expect($scope.comment.$save).toHaveBeenCalled()

Example n°3

Services

angular.module 'pepiniere-dashboard-sirf.board'
.factory 'BoardPim1Service', ($filter, $translate, GlobalFilterService) ->

  computeDeliveryFeesChart = (entities, input) ->

    skeleton =
      data: [
        {
          key: $translate.instant 'widgets.deliveryFees.deliveryFees'
          values: []
        }
      ]
      options:
        ...


    return skeleton if not input or _.isEmpty input
    entity  = GlobalFilterService.get 'entity'
    if not entity
      entity = _.find entities, (item) -> item.position is 0
    data = _.find input, (item) ->
      return entity and entity.name.toLowerCase() is item.department.toLowerCase()
    return skeleton if not data
    skeleton.data[0].values = _.map data.items, (item) ->
      x: new Date item.date
      y: item.value
    minValue = _.min _.map(skeleton.data[0].values, 'y')
    minValue = 100 * Math.ceil(minValue * 0.5 / 100)
    skeleton.options.chart.forceY = [ minValue ]
    return skeleton

  computeDeliveryFeesChart: computeDeliveryFeesChart
describe '[SERVICE] BoardPim1Service', ->

  BoardPim1Service = null
  GlobalFilterService = null

  beforeEach module 'pepiniere-dashboard-sirf.filters'
  beforeEach module 'pepiniere-dashboard-sirf.board'

  beforeEach inject (_BoardPim1Service_, _GlobalFilterService_) ->
    BoardPim1Service     = _BoardPim1Service_
    GlobalFilterService  = _GlobalFilterService_

  it 'should be defined', ->
    expect(BoardPim1Service).toBeDefined()

  describe 'computeChangeItChart', ->

    entities = [{name: 'dir', position: 0}]

    it 'should be defined', ->
      expect(BoardPim1Service.computeDeliveryFeesChart).toBeDefined()
    it 'should return an array object', ->
      result = BoardPim1Service.computeDeliveryFeesChart entities, data
      expect(result.data).toBeDefined()
      expect(result.options).toBeDefined()
      expect(result.data.length).toBe(1)
      expect(result.data[0].values.length).toBe(12)

    it 'should filter signs', ->
      GlobalFilterService.put 'entity', {name: 'DIR', position: 1}
      result = BoardPim1Service.computeDeliveryFeesChart entities, data
      expect(result.data[0].values.length).toBe(12)

    it 'should give a date formater', ->
      result = BoardPim1Service.computeDeliveryFeesChart entities, data
      fn = result.options.chart.xAxis.tickFormat
      expect(fn).toBeDefined()
      expect(fn(new Date(2015, 2,15))).toEqual 'Mar 15'

Unit testing

By Jérémie Drouet

Unit testing

  • 1,122