【问题标题】:How to provide mock files to change event of <input type='file'> for unit testing如何提供模拟文件来更改 <input type='file'> 的事件以进行单元测试
【发布时间】:2014-09-25 16:34:32
【问题描述】:

我在进行单元测试时遇到了困难,我想在其中验证文件的处理,通常通过&lt;input type='file'&gt; 在视图中选择该文件。

在我的 AngularJS 应用程序的控制器部分,文件在输入的更改事件中处理,如下所示:

//bind the change event of the file input and process the selected file
inputElement.on("change", function (evt) {
    var fileList = evt.target.files;
    var selectedFile = fileList[0];
    if (selectedFile.size > 500000) {
        alert('File too big!');
    // ...

我希望evt.target.files 在我的单元测试中包含我的模拟数据,而不是用户选择的文件。我意识到我不能自己实例化 FileListFile 对象,这将是浏览器正在使用的相应对象。因此,我将模拟 FileList 分配给输入的 files 属性并手动触发更改事件:

describe('document upload:', function () {
    var input;

    beforeEach(function () {
        input = angular.element("<input type='file' id='file' accept='image/*'>");
        spyOn(document, 'getElementById').andReturn(input);
        createController();
    });

    it('should check file size of the selected file', function () {
        var file = {
            name: "test.png",
            size: 500001,
            type: "image/png"
        };

        var fileList = {
            0: file,
            length: 1,
            item: function (index) { return file; }
        };

        input.files = fileList; // assign the mock files to the input element 
        input.triggerHandler("change"); // trigger the change event

        expect(window.alert).toHaveBeenCalledWith('File too big!');
    });

不幸的是,这会导致控制器中出现以下错误,表明此尝试失败,因为文件根本没有分配给输入元素:

TypeError: 'undefined' 不是对象(评估 'evt.target.files')

出于安全原因,我已经发现 input.files 属性是只读的。所以我开始了另一种方法,通过调度一个定制的更改来提供 files 属性,但仍然没有成功。

长话短说:我很想学习一个可行的解决方案或任何关于如何处理这个测试用例的最佳实践。

【问题讨论】:

  • 你使用的是 jQuery 还是 jqLit​​e?
  • 在新的 Blobs() 上添加一些道具应该会让你得到一些像 File() 一样嘎嘎作响的东西......

标签: javascript angularjs unit-testing jasmine


【解决方案1】:

更新:感谢@PeteBD,

从 angularjs 版本 1.2.22 开始,jqLit​​e 现在支持将自定义事件对象传递给 triggerHandler()。见:d262378b


如果您只使用 jqLit​​e

triggerHandler() 永远不会工作,因为它会将一个虚拟事件对象传递给处理程序。

虚拟事件对象如下所示(复制自jqLite.js#L962

{
  preventDefault: noop,
  stopPropagation: noop
}

如您所见,它甚至没有 target 属性。

如果您使用的是 jQuery

您可以使用这样的自定义事件对象触发事件:

input.triggerHandler({
  type: 'change',
  target: {
    files: fileList
  }
});

evt.target.files 将是您所期望的fileList

希望这会有所帮助。

【讨论】:

  • 非常感谢您的详尽回答!我渴望一个使用 jqLit​​e(甚至根本没有 jqLit​​e)而不是 jQuery 的工作解决方案,因为我不一定只想为了这个问题而切换到 jQuery。如果没有提出其他可行的解决方案,我会回复您的答案并接受它。再次感谢您!
  • @runTarm 在这里是正确的。 jqLit​​e 应该在这方面由github.com/angular/angular.js/pull/8505修复
  • 我个人认为使用 jQuery 进行测试没有问题...这样您可以保持部署的产品精益并仍然获得 jQuery 可以为测试提供的功能。
【解决方案2】:

让我们重新思考 AngularJS,DOM must be handled in a directive

我们不应该在控制器中处理 DOM 元素,即element.on('change', ..,尤其是用于测试目的。在控制器中,您与数据对话,而不是与 DOM。

因此,那些onchange 应该是如下的指令

<input type="file" name='file' ng-change="fileChanged()" /> <br/>

但是,不幸的是,ng-change 不能很好地与type="file" 配合使用。我不确定未来的版本是否适用于此。我们仍然可以应用相同的方法。

<input type="file" 
  onchange="angular.element(this).scope().fileChanged(this.files)" />

在控制器中,我们只是简单地定义一个方法

$scope.fileChanged = function(files) {
  return files.0.length < 500000;
};

现在,一切都只是普通的控制器测试。无需再处理angular.element$compiletriggers 等! :)

describe(‘MyCtrl’, function() {
  it('does check files', inject(
    function($rootScope, $controller) {
      scope = $rootScope.new();
      ctrl = $controller(‘UploadCtrl’, {‘$scope’: scope});

      var files = { 0: {name:'foo', size: 500001} };
      expect(scope.fileChanged(files)).toBe(true);
    }
  ));
});

http://plnkr.co/edit/1J7ETus0etBLO18FQDhK?p=preview

【讨论】:

  • 注意,我认为 scope() 是一个仅用于调试的函数,并不适合生产代码。
  • 对这个答案有点困惑,因为它首先说“必须在指令中处理”,然后给出一个不使用指令的示例。如果您放入指令(这是一个好建议!),您将立即回到需要触发器/$compile 并找到一种将文件对象传递给您的处理程序的方法。 @runTarm 的回答解释了如何做后者。
  • @user1821052,它首先说“必须处理 DOM...”,然后继续说“我们不应该在控制器中处理 DOM 元素”,表明解决方案没有操作控制器内的 DOM。
【解决方案3】:

这是使用 angular2+ 输入文件/图像的示例规范。

it('should call showError on toastService Api on call of onSaveOfImage() method', () => {

    spyOn(component.commonFacade.fileIOApi, 'uploadFile');
    let file = new File([new ArrayBuffer(2e+5)], 'test-file.jpg', { lastModified: null, type: 'image/jpeg' });
    let fileInput={ files: [file] };
    component['onSaveOfImage'](fileInput,"",null,"","");
    expect(component.commonFacade.fileIOApi.uploadFile).toHaveBeenCalledTimes(1);
    expect(component.uploadedFileData).toBeUndefined();
    expect(component.commonFacade.employeeApi.toastService.showError).toHaveBeenCalledTimes(1);
  })

【讨论】:

    【解决方案4】:

    您的文件更改处理程序可能应该是直接在您的控制器上的一个函数。您可以将该函数绑定到来自 HTML 或指令的更改事件。这样你就可以直接调用你的处理函数而不用担心触发事件。这段 egghead.io 视频介绍了几种方法:https://egghead.io/lessons/angularjs-file-uploads

    在使用 Angular 滚动您自己的文件上传器时,您需要担心很多事情,所以我会使用现有的库之一来处理它。例如angular-file-upload

    【讨论】:

    • 我很清楚这些指令,但它们并不能解决问题,因为我只关心文件输入的这个基本机制(任何指令也会依赖)以及如何使用模拟数据对其进行正确的单元测试。在视图中绑定更改事件并在控制器上使用可以在单元测试中直接调用(而不是触发更改事件)的回调函数会起作用,但通常我希望能够解决这个问题,即使更改事件通过 JavaScript 绑定,如我的代码中所述。所以对我来说问题仍然存在。
    猜你喜欢
    • 2019-01-28
    • 2020-07-21
    • 2012-06-29
    • 2011-04-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-27
    • 2012-05-06
    • 2010-12-28
    相关资源
    最近更新 更多