javascript - 包括 FileReader.onload 在内的 Angular 单元测试不适用于 async/whenStable 和 fakeAsync/tick

标签 javascript angular jasmine karma-jasmine filereader

我正在向我的 Angular 7 应用程序添加一些单元测试,但我不确定如何处理与 FileReader 的交互,因为使用 async/whenStable、fakeAsync 和 promise 不能按预期工作。

  • 我正在测试的行为 : 我需要验证服务documentService.sendDocument文件上传后调用。

  • 第一种方法 :

    在我发现测试 FileReader.onload 的问题之前,我只是尝试在没有异步的情况下对其进行测试。

    Controller 代码
    onFileGovernmentIdChange(event) {
        console.log('::: Init onFileGovernmentIdChange');
        const updateFunction = () => {
            console.log('::: BEFORE update fileGovernmentIdUploaded - ', this.fileGovernmentIdUploaded);
            this.fileGovernmentIdUploaded = true;
            console.log('::: AFTER update fileGovernmentIdUploaded - ', this.fileGovernmentIdUploaded);
        }
        this.saveFileFromInputSimple(event, DOCUMENT_TYPE.STORE_GOVERNMENT_ID, updateFunction);
        console.log('::: End onFileGovernmentIdChange');
    }
    
    private saveFileFromInputSimple(event, documentType: DOCUMENT_TYPE, updateState: () => any) {
        console.log('::: Init saveFileFromInputSimple');
        const reader = new FileReader();
    
        if (event.target.files && event.target.files.length > 0) {
            console.log('::: Event with files...');
            const file = event.target.files[0];
            reader.onload = () => {
                this.documentService
                    .sendDocument(file.name, file.type, reader.result.toString(), documentType)
                    .subscribe(
                        response => {
                            console.log('::: sendDocument - Subscribe OK');
                            updateState()
                        },
                        error => {
                            console.log('::: sendDocument - Subscribe ERROR');
                            this.showDefaultErrorDialog()
                        }
                    );
            };
            console.log('::: Onload callback assigned...');
            reader.readAsDataURL(file);
        }
        console.log('::: End saveFileFromInputSimple');
    }
    
    
    

    UnitTest 规范代码
    fit('simple testing', () => {
        const documentService = TestBed.get(DocumentsService);
        const catalogService: CatalogService = TestBed.get(CatalogService);
        const customEvent = {
            target: {
                files: [new Blob(['ssdfsdgdjghdslkjghdjg'], { type: 'pdf' })]
            }
        };
    
        const commerceResponse = new CommerceResponse();
        commerceResponse.commissionPercentage = '11';
    
        spyOn(catalogService, 'getCatalog').and.returnValue(of({ catalogs: [] }));
        spyOn(documentService, 'getCommerceInfo').and.returnValue(of(commerceResponse));
        spyOn(documentService, 'sendDocument').and.returnValue(of({ response: 'ok' }));
    
        fixture.detectChanges();//Apply onInit changes
        console.log('::: Before calling onFileGovernmentIdChange()');
        component.onFileGovernmentIdChange(customEvent);
        console.log('::: After calling onFileGovernmentIdChange()');
        console.log('::: Before expects... ');
        expect(component.fileGovernmentIdUploaded).toBeTruthy();
        // expect(documentService.sendDocument).toHaveBeenCalledTimes(1);
        console.log('::: After expects... ');
    });
    

    单元测试结果
    LOG: '::: Before calling onFileGovernmentIdChange()'
    LOG: '::: Init onFileGovernmentIdChange'
    LOG: '::: Init saveFileFromInputSimple'
    LOG: '::: Event with files...'
    LOG: '::: Onload callback assigned...'
    LOG: '::: End saveFileFromInputSimple'
    LOG: '::: End onFileGovernmentIdChange'
    LOG: '::: After calling onFileGovernmentIdChange()'
    LOG: '::: Before expects... '
    LOG: '::: After expects... '
    Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent simple testing FAILED
            Expected undefined to be truthy.
                at UserContext.<anonymous> src/app/documents/documents.component.spec.ts:146:52)
                at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
                at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:288:1)
                at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:387:1)
    

    单元测试分析
    onload 内的代码方法从不执行

    第二种方法

    在没有观察者的情况下测试 FileReader.onload:为了遵循工作流程,我删除了 onload 中的观察者验证这是否可能是问题所在。

    Controller 代码
    private saveFileFromInputSimple(event, documentType: DOCUMENT_TYPE, updateState: () => any) {
        console.log('::: Init saveFileFromInputSimple');
        const reader = new FileReader();
    
        if (event.target.files && event.target.files.length > 0) {
            console.log('::: Event with files...');
            const file = event.target.files[0];
            reader.onload = () => {
                console.log('::: ONLOAD executed');
            };
            console.log('::: Onload callback assigned...');
            reader.readAsDataURL(file);
        }
        console.log('::: End saveFileFromInputSimple');
    }
    

    UnitTest 规范代码

    与第一种方法相同

    单元测试结果
    LOG: '::: Before calling onFileGovernmentIdChange()'
    LOG: '::: Init onFileGovernmentIdChange'
    LOG: '::: Init saveFileFromInputSimple'
    LOG: '::: Event with files...'
    LOG: '::: Onload callback assigned...'
    LOG: '::: End saveFileFromInputSimple'
    LOG: '::: End onFileGovernmentIdChange'
    LOG: '::: After calling onFileGovernmentIdChange()'
    LOG: '::: Before expects... '
    LOG: '::: After expects... '
    Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent simple testing FAILED
            Expected undefined to be truthy.
                at UserContext.<anonymous> src/app/documents/documents.component.spec.ts:146:52)
                at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
                at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:288:1)
                at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:387:1)
    Chrome 71.0.3578 (Mac OS X 10.13.6): Executed 1 of 46 (1 FAILED) (0 secs / 0.314 secs)
    Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent simple testing FAILED
            Expected undefined to be truthy.
                at UserContext.<anonymous> src/app/documents/documents.component.spec.ts:146:52)
                at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
                at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:288:1)
    LOG: '::: ONLOAD executed'
    

    单元测试分析

    现在 onload 中的代码方法已执行,但看起来像一个异步操作,因为它是在最后执行的。

    第三种方法

    使用异步单元测试测试 FileReader.onload:当我发现某种异步操作时,我包含了 async/fixture.whenStable Angular 组合等待异步代码完成。

    Controller 代码

    与第二种方法相同

    UnitTest 规范代码
    fit('Testing with async/fixture.whenStable', async(() => {
        const documentService = TestBed.get(DocumentsService);
        const catalogService: CatalogService = TestBed.get(CatalogService);
        const customEvent = {
            target: {
                files: [new Blob(['ssdfsdgdjghdslkjghdjg'], { type: 'pdf' })]
            }
        };
    
        const commerceResponse = new CommerceResponse();
        commerceResponse.commissionPercentage = '11';
    
        spyOn(catalogService, 'getCatalog').and.returnValue(of({ catalogs: [] }));
        spyOn(documentService, 'getCommerceInfo').and.returnValue(of(commerceResponse));
        spyOn(documentService, 'sendDocument').and.returnValue(of({ response: 'ok' }));
    
        fixture.detectChanges();//Apply onInit changes
        console.log('::: Before calling onFileGovernmentIdChange()');
        component.onFileGovernmentIdChange(customEvent);
        console.log('::: After calling onFileGovernmentIdChange()');
        console.log('::: Before expects... ');
    
        fixture.whenStable().then(() => {
            fixture.detectChanges();
            console.log('::: whenStable Init');
            expect(component.fileGovernmentIdUploaded).toBeTruthy();
            // expect(documentService.sendDocument).toHaveBeenCalledTimes(1);
            console.log('::: whenStable End');
        });
    
        console.log('::: After expects... ');
    }));
    

    单元测试结果
    LOG: '::: Before calling onFileGovernmentIdChange()'
    LOG: '::: Init onFileGovernmentIdChange'
    LOG: '::: Init saveFileFromInputSimple'
    LOG: '::: Event with files...'
    LOG: '::: Onload callback assigned...'
    LOG: '::: End saveFileFromInputSimple'
    LOG: '::: End onFileGovernmentIdChange'
    LOG: '::: After calling onFileGovernmentIdChange()'
    LOG: '::: Before expects... '
    LOG: '::: After expects... '
    LOG: '::: whenStable Init'
    LOG: '::: whenStable End'
    **LOG: '::: ONLOAD executed'**
    

    单元测试分析

    正如预期的那样,whenStable 代码在方法完成时执行,然而,onload方法最后继续执行。
    谷歌搜索我发现最好将 onload 部分包装在 Promise 中,以确保它被 Angular 异步跟踪。

    第四种方法 :

    包裹onload在一个 promise 中并验证它是否适用于 async/whenStable结构体。

    Controller 代码
    private readFileAsync(file): Promise<string> {
        console.log('::: readFileAsync Init');
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => {
                console.log(':::: Promise Resolved in ONLOAD')
                resolve(reader.result.toString());
            }
            reader.onerror = reject;
            reader.readAsDataURL(file);
        });
    }
    
    private saveFileFromInputWithPromise(event, documentType: DOCUMENT_TYPE, updateState: () => any) {
        console.log('::: Init saveFileFromInputWithPromise');
    
        if (event.target.files && event.target.files.length > 0) {
            console.log('::: Event with files...');
            const file = event.target.files[0];
    
            this.readFileAsync(file)
                .then(fileContent => {
                    console.log('::: File with content', fileContent);
                    updateState();
                }).catch(error => console.log('::: File load error', error));
        }
        console.log('::: End saveFileFromInputWithPromise');
    }
    

    UnitTest 规范代码

    与第三种方法相同

    单元测试结果
    LOG: '::: Before calling onFileGovernmentIdChange()'
    LOG: '::: Init onFileGovernmentIdChange'
    LOG: '::: Init saveFileFromInputWithPromise'
    LOG: '::: Event with files...'
    LOG: '::: readFileAsync Init'
    LOG: '::: End saveFileFromInputWithPromise'
    LOG: '::: End onFileGovernmentIdChange'
    LOG: '::: After calling onFileGovernmentIdChange()'
    LOG: '::: Before expects... '
    LOG: '::: After expects... '
    LOG: '::: whenStable Init'
    LOG: '::: whenStable End'
    LOG: ':::: Promise Resolved in ONLOAD'
    LOG: '::: File with content', 'data:pdf;base64,c3NkZnNkZ2RqZ2hkc2xramdoZGpn'
    LOG: '::: BEFORE update fileGovernmentIdUploaded - ', undefined
    LOG: '::: AFTER update fileGovernmentIdUploaded - ', true
    Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent Testing with async/fixture.whenStable FAILED
            Expected undefined to be truthy.
                at http://localhost:9877/_karma_webpack_/webpack:/src/app/documents/documents.component.spec.ts:176:56
                at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
                at AsyncTestZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.AsyncTestZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:713:1)
                at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:285:1)
    
    

    单元测试分析

    whenStable 之后再次解决了 Promise功能。

    第五进场 :

    作为 async/whenStable没有按预期工作我尝试更改为 fakeAsync/tick/flush结构体。

    Controller 代码

    与第 4 种方法相同(包括 onload 在 promise 中)

    UnitTest 规范代码
    fit('Testing with fakeAsync/tick/flush', fakeAsync(() => {
        const documentService = TestBed.get(DocumentsService);
        const catalogService: CatalogService = TestBed.get(CatalogService);
        const customEvent = {
            target: {
                files: [new Blob(['ssdfsdgdjghdslkjghdjg'], { type: 'pdf' })]
            }
        };
    
        const commerceResponse = new CommerceResponse();
        commerceResponse.commissionPercentage = '11';
    
        spyOn(catalogService, 'getCatalog').and.returnValue(of({ catalogs: [] }));
        spyOn(documentService, 'getCommerceInfo').and.returnValue(of(commerceResponse));
        spyOn(documentService, 'sendDocument').and.returnValue(of({ response: 'ok' }));
    
        fixture.detectChanges();
        console.log('::: Before calling onFileGovernmentIdChange()');
        component.onFileGovernmentIdChange(customEvent);
        console.log('::: After calling onFileGovernmentIdChange()');
        console.log('::: Before expects... ');
    
        fixture.detectChanges();
        tick();
        flushMicrotasks();
        flush();
        console.log('::: After Flush Init');
        expect(component.fileGovernmentIdUploaded).toBeTruthy();
        // expect(documentService.sendDocument).toHaveBeenCalledTimes(1);
        console.log('::: After Flush End');
        console.log('::: After expects... ');
    }));
    

    单元测试结果
    LOG: '::: Before calling onFileGovernmentIdChange()
    LOG: '::: Init onFileGovernmentIdChange'
    LOG: '::: Init saveFileFromInputWithPromise'
    LOG: '::: Event with files...'
    LOG: '::: readFileAsync Init'
    LOG: '::: End saveFileFromInputWithPromise'
    LOG: '::: End onFileGovernmentIdChange'
    LOG: '::: After calling onFileGovernmentIdChange()'
    LOG: '::: Before expects... '
    LOG: '::: After Flush Init'
    LOG: '::: After Flush End'
    LOG: '::: After expects... '
    Chrome 71.0.3578 (Mac OS X 10.13.6) DocumentsComponent Testing with async/fixture.whenStable FAILED
            Expected undefined to be truthy.
                at UserContext.<anonymous> src/app/documents/documents.component.spec.ts:211:52)
                at UserContext.<anonymous> node_modules/zone.js/dist/zone-testing.js:1424:1)
                at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invoke node_modules/zone.js/dist/zone.js:388:1)
                at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke node_modules/zone.js/dist/zone-testing.js:288:1)
    LOG: ':::: Promise Resolved in ONLOAD'
    LOG: '::: File with content', 'data:pdf;base64,c3NkZnNkZ2RqZ2hkc2xramdoZGpn'
    LOG: '::: BEFORE update fileGovernmentIdUploaded - ', undefined
    LOG: '::: AFTER update fileGovernmentIdUploaded - ', true
    

    单元测试分析

    同样,在 fakeAsync/tick/flushMicrotasks/flush 结构之后解决了 Promise

    怎么了?

    我遵循了我找到的每个教程以及尝试包含 FileReader.onload 的所有不同方法。在我的测试中(因为该方法调用我想要监视和验证的服务),但总是在 Angular 提供的异步 block 之后解析该方法。我看到了 window.fileReader 的其他方法。被 mock ,但这不是我测试的目的。

    那么有人可以告诉我我的代码或我正在测试的方式有什么问题吗?

    最佳答案

    很棒的帖子,完美地描述了我的夜晚。

    fakeAsync 不支持 FileReader 似乎是一个已知问题,因为“它根本不是与计时器相关的异步操作”。

    见:https://github.com/angular/zone.js/issues/1120

    关于javascript - 包括 FileReader.onload 在内的 Angular 单元测试不适用于 async/whenStable 和 fakeAsync/tick,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54612307/

    相关文章:

    javascript - 有没有办法在引导 TreeView 中更改节点文本?

    javascript滚动事件不一致

    javascript - 使用 Google Maps API 时出现 "Uncaught ReferenceError: google is not defined"错误

    angular - 当我路由到不同的组件时,我可以在 Angular 7 的后台调用 api 服务吗

    angular-cli - 单元测试无法在 VSTS 中运行?

    javascript - 如何在html5中填充纹理(并且仍然能够改变颜色?)

    angular - 检查函数是否被调用,Angular 单元测试

    css - 使用 Border-bottom 将悬停/事件链接放置在菜单项的底部

    unit-testing - Angularjs:angular-phonecat 教程应用程序第一次单元测试失败

    angularjs - Jasmine 提示 Angular 模块中不需要它们的依赖关系