【问题标题】:How to unit test / mock a $timeout call?如何对 $timeout 调用进行单元测试/模拟?
【发布时间】:2018-08-31 00:06:27
【问题描述】:

我如何在这里模拟超时调用?

$scope.submitRequest = function () {

    var formData = getData();

    $scope.form = JSON.parse(formData);

    $timeout(function () {
        $('#submitForm').click();            
    }, 2000);

};

我想看看 timeout 是否被正确的函数调用了。

我想要一个模拟 $timeout 的 spyon 函数示例。

spyOn(someObject,'$timeout')

【问题讨论】:

    标签: angularjs unit-testing jasmine mocking spyon


    【解决方案1】:

    我完全同意Frane Poljakanswer。你当然应该按照他的方式行事。第二种方法是模拟 $timeout 服务,如下所示:

    describe('MainController', function() {
    var $scope, $timeout;
    
    beforeEach(module('app'));
    
    beforeEach(inject(function($rootScope, $controller, $injector) {
      $scope = $rootScope.$new();
      $timeout = jasmine.createSpy('$timeout');
      $controller('MainController', {
        $scope: $scope,
        $timeout: $timeout
      });
    }));
    
    it('should submit request', function() {
      $scope.submitRequest();
      expect($timeout).toHaveBeenCalled();
    });
    

    这是具有两种方法的 plunker:http://plnkr.co/edit/s5ls11

    【讨论】:

    • 我不会推荐这种方法。 $timeout 已经是 ngMock 模块的一部分
    • 同意,不建议使用这种方法,因为 ngMock 使用 flush 和 verifyNoPendingTasks 装饰 $timeout 服务,因此鼓励它们。这只是一种可能。
    【解决方案2】:

    $timeout 可以被窥探或模拟,如this answer 所示:

    beforeEach(module('app', ($provide) => {
      $provide.decorator('$timeout', ($delegate) => {
        var timeoutSpy = jasmine.createSpy().and.returnValue($delegate);
        // methods aren't copied automatically to spy
        return angular.extend(timeoutSpy, $delegate);
      });
    }));
    

    这里没有太多要测试的东西,因为$timeout 是用匿名函数调用的。出于可测试性的原因,将其公开为范围/控制器方法是有意义的:

    $scope.submitFormHandler = function () {
        $('#submitForm').click();            
    };
    
    ...
    $timeout($scope.submitFormHandler, 2000);
    

    那么窥探到$timeout就可以测试了:

    $timeout.and.stub(); // in case we want to test submitFormHandler separately
    scope.submitRequest();
    expect($timeout).toHaveBeenCalledWith(scope.submitFormHandler, 2000);
    

    并且$scope.submitFormHandler里面的逻辑可以在不同的测试中测试。

    这里的另一个问题是 jQuery 在单元测试中不能很好地工作,并且需要针对真实的 DOM 进行测试(这是在 AngularJS 应用程序中尽可能避免使用 jQuery 的众多原因之一)。可以像 this answer 中所示的那样监视/模拟 jQuery API。

    $(...) 调用可以被监听:

    var init = jQuery.prototype.init.bind(jQuery.prototype);
    spyOn(jQuery.prototype, 'init').and.callFake(init);
    

    并且可以被嘲笑:

    var clickSpy = jasmine.createSpy('click');
    spyOn(jQuery.prototype, 'init').and.returnValue({ click: clickSpy });
    

    请注意,预计模拟函数将返回 jQuery 对象以与 click 方法链接。

    $(...) 被模拟时,测试不需要在 DOM 中创建 #submitForm 夹具,这是隔离单元测试的首选方式。

    【讨论】:

      【解决方案3】:

      单元测试 $timeout 与刷新延迟

      你必须通过调用 $timeout.flush() 来刷新 $timeout 服务的队列

      describe('controller: myController', function(){
      describe('showAlert', function(){
          beforeEach(function(){
              // Arrange
              vm.alertVisible = false;
      
              // Act
              vm.showAlert('test alert message');
          });
      
          it('should show the alert', function(){
              // Assert
              assert.isTrue(vm.alertVisible);
          });
      
          it('should hide the alert after 5 seconds', function(){
              // Act - flush $timeout queue to fire off deferred function
              $timeout.flush();
      
              // Assert
              assert.isFalse(vm.alertVisible);
          });
        })
      });
      

      请查看此链接http://jasonwatmore.com/post/2015/03/06/angularjs-unit-testing-code-that-uses-timeout

      【讨论】:

        【解决方案4】:

        假设这段代码在控制器中或者是由 $controller 在测试中创建的,那么 $timeout 可以在构造参数中传递。所以你可以这样做:

        var timeoutStub = sinon.stub();
        var myController = $controller('controllerName', timeoutStub);
        $scope.submitRequest();
        expect(timeoutStub).to.have.been.called;
        

        【讨论】:

          【解决方案5】:

          为 $timeout 提供者创建模拟:

          var f = () => {} 
          var myTimeoutProviderMock = () => f;
          

          使用它:

          beforeEach(angular.mock.module('myModule', ($provide) => {
            $provide.factory('$timeout', myTimeoutProviderMock);
          }))
          

          现在你可以测试了:

          spyOn(f);
          expect(f).toHaveBeenCalled();
          

          附:你最好在超时时测试函数的结果。

          【讨论】:

          • spyOn(f) 不起作用。 myTimeoutProviderMock 将继续使用原来的f
          【解决方案6】:

          首先,DOM 操作只能在指令中执行。 此外,最好使用 angular.element(...),而不是 $(...)。 最后,要做到这一点,您可以将元素的点击处理程序暴露给作用域,监视它,并检查该处理程序是否已被调用:

          $timeout.flush(2000);
          $timeout.verifyNoPendingTasks();
          expect(scope.myClickHandler).toHaveBeenCalled();
          

          编辑:

          因为这是一个表单并且没有 ng-click 处理程序,您可以使用 ng-submit 处理程序,或者为您的表单添加一个名称并执行以下操作:

          $timeout.flush(2000);
          $timeout.verifyNoPendingTasks();
          expect(scope.formName.$submitted).toBeTruthy();
          

          【讨论】:

          • 正在提交表单,没有点击处理程序。我想我可以有一个点击处理程序,但这有点难看。
          猜你喜欢
          • 2019-08-22
          • 1970-01-01
          • 2018-04-04
          • 2021-09-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-08-19
          相关资源
          最近更新 更多