【问题标题】:How to wait for a callback to be executed inside a AngularJS directive unit test?如何等待在 AngularJS 指令单元测试中执行回调?
【发布时间】:2015-06-12 11:34:50
【问题描述】:

我有一个可以正常工作的简单指令(使用 TypeScript 编写,但并不重要),它允许获取通过 <input type="file"> 和 HTML5 File API 上传的文件的内容。

使用简单:

<body ng-controller="MyCtrl as ctrl">
  <input type="file" file-read="ctrl.readInputFile(data)">
</body>

正在使用文件内容调用回调 readInputFile()。

interface IFileReadScope extends angular.IScope {
  fileRead(data: any): void;
}

class FileRead implements angular.IDirective {
  restrict = 'A';
  scope = {
    fileRead: '&'
  };

  link = (scope: IFileReadScope, element: angular.IAugmentedJQuery) => {
    element.on('change', (e: Event) => {
      const files: FileList = (<HTMLInputElement> e.target).files;

      for (let i = 0; i < files.length; i++) {
        const file = files[i];

        const reader = new FileReader();

        reader.onload = (e: Event) => {
          scope.$apply(() => {
            scope.fileRead({data: (<FileReader> e.target).result});
          });
        };

        reader.readAsText(file);
      }
    });
  };

  static factory(): angular.IDirectiveFactory {
    const directive = () => new FileRead();
    return directive;
  }
}

app.directive('fileRead', FileRead.factory());

这里是对它的测试:

describe('fileRead', () => {
  beforeEach(module('app'));

  let $compile: angular.ICompileService;
  let scope: any;

  beforeEach(inject((_$compile_, _$rootScope_) => {
    $compile = _$compile_;
    scope = _$rootScope_.$new();
  }));

  function compileTemplate(template: string): angular.IAugmentedJQuery {
    const el = $compile(angular.element(template))(scope);
    scope.$digest();
    return el;
  }

  it('should call a callback each time a file has been read', () => {
    const el = compileTemplate(
      '<input type="file" file-read="readInputFile(data)">'
    );

    const files = new Array<string>();

    scope.readInputFile = (data: any) => {
      files.push(data);
    };

    el.triggerHandler({
      type: 'change',
      target: {
        files: [
          new Blob(['data0']),
          new Blob(['data1'])
        ]
      }
    });

    scope.$digest();

    // Fails because it does not wait for scope.readInputFile() to be called
    expect(files.length).toEqual(2);
    expect(files[0]).toEqual('data0');
    expect(files[1]).toEqual('data1');
  });
});

问题是我不知道如何等待scope.readInputFile()回调被执行。

我已经尝试过 scope.$digest()、$rootScope.$digest()、$apply、Jasmine spyOn... 我还检查了流行的指令是如何做到这一点的。似乎没有人这样做:/

有什么想法吗?

编辑:

感谢 Hooray Im Helping 和 Matho,这里有两种使用 Jasmine 2 的可能解决方案:

let files: string[];

beforeEach(done => {
  const el = compileTemplate(
    '<input type="file" file-read="readInputFile(data)">'
  );

  files = new Array<string>();

  scope.readInputFile = (data: any) => {
    files.push(data);
    if (files.length === 2) done();
  };

  el.triggerHandler({
    type: 'change',
    target: {
      files: [
        new Blob(['data0']),
        new Blob(['data1'])
      ]
    }
  });

  scope.$digest();
});

it('should call a callback each time a file has been read', () => {
  expect(files.length).toEqual(2);
  expect(files[0]).toEqual('data0');
  expect(files[1]).toEqual('data1');
});

在这种情况下更优雅:

it('should call a callback each time a file has been read', done => {
  const el = compileTemplate(
    '<input type="file" file-read="readInputFile(data)">'
  );

  const files = new Array<string>();

  scope.readInputFile = (data: any) => {
    files.push(data);
    if (files.length === 2) {
      expect(files[0]).toEqual('data0');
      expect(files[1]).toEqual('data1');
      done();
    }
  };

  el.triggerHandler({
    type: 'change',
    target: {
      files: [
        new Blob(['data0']),
        new Blob(['data1'])
      ]
    }
  });

  scope.$digest();
});

如果scope.fileRead()没有在指令内部执行,那么测试将失败:

FAILED fileRead should call a callback each time a file has been read
Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.

因此,如果代码中断,单元测试将失败 => 工作完成 :)

【问题讨论】:

    标签: angularjs typescript jasmine karma-jasmine


    【解决方案1】:

    我不使用 Jasime,我总是使用 mocha + chaijs + sinon,但 jasmine 也实现了 done 函数,它应该像这样工作:

    it('async method', function(done){ 
    //once we add the done param, karma will wait until the fn is executed
    
     service.doSomethingAsync()
      .then(function(){
        //assertions 
        // expect(result).to.be.something();
        done(); //Where we are telling karma that it can continue with the test
      }
      //Other assertions    
    }
    

    使用chai-as-promised

    it('async method', function(done){
    
     var result = service.doSomethingAsync();
     expect(result).to.eventually.be(something=;
      //Other assertions    
    }
    

    【讨论】:

      【解决方案2】:

      你用的是什么版本的 Jasmine?

      如果是 1.3,你可以使用waitsFor and runs 等待一段时间然后在规范上断言:

      所以

      let files = new Array<string>();
      
      scope.readInputFile = (data: any) => {
        files.push(data);
      };
      
      el.triggerHandler({
        type: 'change',
        target: {
          files: [
            new Blob(['data0']),
            new Blob(['data1'])
          ]
        }
      });
      
      scope.$digest();
      

      变成

      let files = new Array<string>();
      
      runs(function() {
          scope.readInputFile = (data: any) => {
              files.push(data);
          };
      
          el.triggerHandler({
              type: 'change',
              target: {
                  files: [
                      new Blob(['data0']),
                      new Blob(['data1'])
                  ]
              }
          });
      
          scope.$digest();
      });
      

      expect(files.length).toEqual(2);
      expect(files[0]).toEqual('data0');
      expect(files[1]).toEqual('data1');
      

      变成

      waitsFor(function() {
          expect(files.length).toEqual(2);
          expect(files[0]).toEqual('data0');
          expect(files[1]).toEqual('data1');
      }, "failure message", time_in_milliseconds);
      

      如果您使用的是 Jasmine 2.0,则可以使用 done 函数:

      您将尝试测试的代码(以及它所依赖的任何设置代码)移动到 beforeEach,在那里调用异步代码,然后通过调用 @987654332 通知框架异步操作已完成@ 异步代码完成时的方法。在调用done 后,您的规范会自动运行。我以前从未使用过这个(我还在 1.3 上)而且我不使用 Angular 或 Karma,所以我处于一个不熟悉的领域。

      根据this answer,您的beforeEach 函数应如下所示:

      beforeEach(function(done) {
          inject((_$compile_: angular.ICompileService, _$rootScope_: angular.IRootScopeService) => {
              $compile = _$compile_;
              $rootScope = _$rootScope_;
              scope = $rootScope.$new();
          });
          let el = compileTemplate('<input type="file" file-read="readInputFile(data)">');
          let files = new Array<string>();
      
          scope.readInputFile = (data: any) => {
              files.push(data);
          };
      
          el.triggerHandler({
              type: 'change',
              target: {
                  files: [
                      new Blob(['data0']),
                      new Blob(['data1'])
                  ]
              }
          });
      
          scope.$digest();
      
          done();
      });
      

      你的断言应该是这样的:

      it('should call a callback each time a file has been read by FileReader', (done) => {
          expect(files.length).toEqual(2);
          expect(files[0]).toEqual('data0');
          expect(files[1]).toEqual('data1');
      
          done();
      });
      

      同样,我不使用 angular、TypeScript 或 Karma,因此您可能需要对此进行调整。但这是一般的想法;设置一切,运行异步代码,在设置中调用done,运行断言,在规范中调用done

      【讨论】:

        【解决方案3】:

        您可以将您的expectations 放入回调函数中。

        【讨论】:

        • 测试将在回调不再被调用的那一天静默失败 => 你只是扼杀了测试代码的全部目的。
        猜你喜欢
        • 1970-01-01
        • 2015-01-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-11-13
        • 2014-08-06
        • 1970-01-01
        相关资源
        最近更新 更多