AngularJS:如何测试具有许多依赖项的指令

时间:2014-06-04 14:09:43

标签: angularjs unit-testing coffeescript angularjs-directive

我试图测试指令几天,我找不到任何好方法。

一些背景信息:我们正在使用Facebook登录并在bootstrap获取好友列表。此朋友列表存储在名为UserConnection的服务中。这项服务有很多方法,但这里有两个重要问题

'use strict'

angular.module('AngularApp')
  .service 'UserConnection', ['Configuration', 'localStorageService', 'Logger', '$location', '$q', '$timeout', '$rootScope', (Configuration, localStorageService, Logger, $location, $q, $timeout, $rootScope) ->
    userConnection =
      facebook                    : {}
      friendsList                 : {}

    services = {}

    #
    # Store the Facebook friends list
    #
    services.updateFBFriendsList = (_friendsList_) ->
      lastInteractions = services.getLastUserInteractions()
      friendsList = angular.copy(_friendsList_)
      _.each(friendsList, (friend) ->

        # Add locale to user
        lang = friend.locale.split("_")[0]
        if _.contains(userConnection.translation.availableLanguages, lang)
          friend.lang = lang
        else
          friend.lang = userConnection.translation.defaultLanguage
        delete friend.locale

        # Add firstName
        friend.firstname = friend.first_name
        delete friend.first_name

        # Add recentInteraction
        interaction = _.indexOf(lastInteractions, friend.third_party_id)
        friend.recentInteraction = if interaction >= 0 then interaction else false
      )

      userConnection.friendsList = _.indexBy(friendsList, 'third_party_id')


    #
    # Return every user friends, or one if an attribute is specified
    #
    services.getFBFriendsList = (specificAttribute = null) ->
      if specificAttribute
        return userConnection.friendsList[specificAttribute]
      else
        return userConnection.friendsList

    return services

  ]

让我们回到我想测试的指令。我想测试函数updateLockers。在这个函数中,我调用UserConnection服务来检查朋友列表中是否存在用户。

'use strict'

angular.module('AngularApp')
  .directive 'lockers', ['Navigation', 'Backend', 'Tools', 'UserConnection',
  (Navigation, Backend, Tools, UserConnection) ->

    replace: true
    restrict: 'A'
    scope: true
    templateUrl: 'views/directives/lockers.html'


    link: ($scope, $elem, $attrs) ->
      directiveId     = "locker"
      directiveReady  = false

      # $scope variables
      $scope.lockers = []
      $scope.active  = false
      $scope.open    = false
      $scope.delete  = false


###################################
# Begin : Handling remote control #
###################################

      # Close the menu
      close = () ->
        $scope.open    = false
        # If the menu was in deleting mode, we remove this
        $scope.delete  = false

      # Open the menu
      open = () ->
        updateLockers()
        $scope.open = true

      # Activate the module
      activate = () ->
        $scope.active = true

      # Desactivate the module
      desactivate = () ->
        $scope.active = false
        close()

      updateNavigation = () ->
        if Navigation.navigationActiveStatus("navigation")
          # If navigation should be activated
          activate()

          # If this module should be open
          if Navigation.navigationOpeningStatus(directiveId)
            open()
          else
            close()

          # If this module should be refreshed
          if Navigation.refreshNavigationStatus(directiveId)
            updateLockers()

        else
          # Navigation must be desactivated
          desactivate()

      $scope.toggle = () ->
        if $scope.open
          Navigation.closeNavigation(directiveId)
        else
          Navigation.openNavigation(directiveId)

      $scope.toggleDelete = () ->
        $scope.delete = !$scope.delete

      # If a component has updated the navigation status, we update
      # the activation status
      $scope.$on 'Navigation:statusUpdated', () ->
        updateNavigation()


#################################
# End : Handling remote control #
#################################


      # Test if there is no more locker in the list
      $scope.isNotEmptyAndActive = () ->
        return $scope.active and $scope.lockers.length > 0


      $scope.buttonClick = (index, toDelete) ->
        if toDelete
          lockerToDelete = $scope.lockers[index]

          Backend.transferStatusToCanceled(lockerToDelete.transfer_id).then(
            (success) ->
              $scope.lockers.splice(index, 1)

              # Close the module is there is no more locker in the list
              Navigation.closeNavigation(directiveId) unless $scope.isNotEmptyAndActive()
          )
        else
          lockerToDownload = $scope.lockers[index]
          UserConnection.redirectToTransferPage(lockerToDownload.sender, lockerToDownload.transfer_id, "internal")



      # Call Backend server to get the last lockers list
      updateLockers = (force = false) ->
        if $scope.active or force
          Backend.getLockerFiles().then(
            (lockers) ->
              $scope.lockers.length = 0
              _.each lockers, (locker) ->
                # We add the transfer only if the sender is still friend with the user
                if UserConnection.getFBFriendsList(locker.sender)
                  # Add the complete sender details
                  locker.senderDetails = UserConnection.getFBFriendsList(locker.sender)
                  locker.humanReadableFileSize = Tools.humanReadableFileSize(locker.file_size)
                  locker.fileType = Tools.getFileType(locker.file_name)
                  locker.fileExt = Tools.getFileExtension(locker.file_name)
                  $scope.lockers.push locker

              if not directiveReady
                # Now the menu is ready to be displayed
                Navigation.directiveReady(directiveId)
                directiveReady = true
          )


      # We load lockers list when user is connected to Backend server
      $scope.$on 'Facebook:userFriendsListLoaded', () ->
        updateLockers(true)

  ]

当我测试函数updateLockers时,我们需要在UserConnection中设置Facebook好友列表并获取此列表,我需要在Facebook上登录,在我们的后端登录等等...

我显然想避免这种情况,所以我考虑为UserConnection服务创建一个Mock。然而,这项服务是巨大的(我在这里只提出了两种方法),并且嘲笑它将是一项巨大的工作。

我无法相信没有更好的解决方案。我错过了什么吗?是否有一个简单的替代方法来避免嘲笑这项服务?也许单元测试策略是完全错误的...感谢您的建议

1 个答案:

答案 0 :(得分:1)

经过多次挖掘并受到@andersschuller的启发,我找到了一个使用Jasmine间谍的简单而令人满意的解决方案。

这是我的单元测试。

"use strict"

describe "UT: Directive Locker", ->
  $scope       = undefined
  $element     = undefined
  $element     = undefined

  Navigation     = undefined
  Backend        = undefined
  UserConnection = undefined
  $q             = undefined

  html = '<div lockers></div>'

  compileDirective = () ->
    inject ($compile, $rootScope) ->
      scope = $rootScope
      $element = $compile(html)(scope)
      scope.$digest()
      $scope = $element.scope()
      Navigation._setScope($scope)


  beforeEach ->
    angular.mock.module('AngularApp')
    angular.mock.module('views/directives/lockers.html')

    module ($provide) ->
      $provide.value "Navigation", new NavigationMock
      return

    inject (_Navigation_, _Backend_, _UserConnection_, _$q_) ->
      Navigation = _Navigation_
      Backend = _Backend_
      UserConnection = _UserConnection_
      $q = _$q_

    compileDirective()


  it "should call updateLocker function when Facebook user friends list is loaded", ->
    spyOn(Backend, 'getLockerFiles')
    $scope.$emit('Facebook:userFriendsListLoaded')
    expect(Backend.getLockerFiles).toHaveBeenCalled()

  it "should display lockers inside the list", ->
    spyOn(Backend, 'getLockerFiles').andCallFake ->
      deferred = $q.defer()
      data = [
        {"transfer_id":"538dc0a0fe33db7cc1000001","expiry":3,"created_at":"2014-06-03T12:33:36Z","sender":"tMWyiUflzB5Yg3pC9oEb9JtIi7I","recipient":"vguYIU4KCRQ-Ah0Lz_dq0EKPIi8","file_name":"polnisch P1.pdf","file_size":244965,"chunk_size":1048576,},
        {"transfer_id":"538dc0acfe33dbeea7000001","expiry":3,"created_at":"2014-06-03T12:33:48Z","sender":"tMWyiUflzB5Yg3pC9oEb9JtIi7I","recipient":"vguYIU4KCRQ-Ah0Lz_dq0EKPIi8","file_name":"polnisch P2.pdf","file_size":245193,"chunk_size":1048576,}
      ]
      deferred.resolve(data)
      return deferred.promise

    spyOn(UserConnection, 'getFBFriendsList').andCallFake ->
      return true

    $scope.$emit('Facebook:userFriendsListLoaded')
    $scope.$digest()

    lockers = $element.find(".lockers-list .locker")
    expect(lockers.length).toEqual(2)

通过在函数调用上添加一个Spy并添加andCallFake,我可以返回一个包含此测试所需数据的promise。然后,我不必模拟完整的服务,并根据执行的测试使系统更改值。