使用 Angular 和 Jasmin 监视 Dialog 和 Observable

Spying on Dialog and Observable with Angular and Jasmin

我正在尝试测试一个相当大的服务方法。我暂时无法更改此方法。

downloadRow(event: any, cell: any, mult_download: boolean): void {
        const maxDownloadSize = this.propsService.getProperty('maxDownloadSize');

        if (!mult_download) {
            const observationSize = cell.getData().file_size;
            const observation_oid = cell.getData().observation_oid;

            console.log(mult_download);
            console.log(maxDownloadSize);
            console.log(observationSize);
            console.log(observation_oid);

            debugger;
            // async job for retrieval if >=2GB
            if (observationSize / maxDownloadSize >= 1) {
                console.log("HERE");
                const bodyDialog = this.body_async.replace(
                    '@size@',
                    this.conversionsService.formatFileSize(observationSize),
                );
                this.dialogService.open(ConfirmDialogComponent, {
                    context: {
                        header: this.header,
                        body: bodyDialog,
                    },
                    hasBackdrop: false,
                    hasScroll: true,
                })
                    .onClose.subscribe((confirm) =>
                        this.doAsyncDownloadFile(confirm, observation_oid),
                    );
                // direct download
            } else {
                this.doDownloadFile(true, observation_oid);
            }
        // multiple download cell has an array of objects(rows to download)
        } else if (mult_download) {
            // console.log('downloadRow mult_download');

            let totsiz = 0;
            let jobType = false;
            const list_of_observationOids = [];
            let num_files = 0;

            for (const row in cell) {
                if ({}.hasOwnProperty.call(cell, row)) {
                    totsiz += +cell[row].file_size;
                    list_of_observationOids.push(`${cell[row].observation_oid} \n`.trim());
                    num_files++;
                }
            }
            // console.log('downloadRow mult_download ' + String(list_of_observationOids));

            if (totsiz / maxDownloadSize >= 1) {
                jobType = true;
            }

            if (jobType) {
                console.log("HERE AGAIN1")
                const bodyDialog = this.body_mult_files.replace(
                    '@files@',
                    String(list_of_observationOids),
                );
                this.dialogService.open(ConfirmDialogComponent, {
                    context: {
                        header: this.header_mult.replace(
                            '@num_files@',
                            String(num_files)),
                        body: bodyDialog,
                        body_mult: this.body_mult,
                    },
                    hasBackdrop: false,
                    hasScroll: true,
                })
                    .onClose.subscribe((confirm) =>
                        this.doAsyncMultDownloadFile(confirm, list_of_observationOids),
                    );
            } else if (!jobType) {
                console.log("HERE AGAIN")
                this.doSyncMultDownloadFile(true, list_of_observationOids);
            }
        }
    }

这是测试。最简单的情况是检查对话框是否为第一个 if 分支打开。

import { SsaQueriesHelperService } from './ssa-queries-helper.service';
import { AppTabulatorService } from './app-tabulator.service';
import { ConversionsService } from './conversions.service';
import { ConfirmDialogComponent } from '@shared/components/confirm-dialog.component';
import {
    NbToastrService,
    NbDialogService
} from '@nebular/theme';
import { PropertiesService } from './properties.service';

class DataRow {
    getData() {
        return { file_size: 3000000000, observation_oid: 194187682 };
    }
}

let service: AppTabulatorService;
let dialogService: jasmine.SpyObj<NbDialogService>;
let propsService: jasmine.SpyObj<PropertiesService>;
let conversionService: jasmine.SpyObj<ConversionsService>;

describe('AppTabulatorService', () => {

    const toastrServiceSpy = jasmine.createSpyObj('NbToastrService', ['show']);
    const dialogServiceSpy = jasmine.createSpyObj('NbDialogService', ['open']);
    const queriesHelperSpy = jasmine.createSpyObj('SsaQueriesHelperService', [
        'createPostcardDownloadUrl',
        'createHighResolutionPostcardDownloadUrl',
        'timeSeriesQuery',
        'createProductDownloadUrl'
    ]);
    const conversionsServiceSpy = jasmine.createSpyObj('ConversionsService', ['formatFileSize']);
    const propsServiceSpy = jasmine.createSpyObj('PropertiesService', ['getProperty']);

    beforeEach(() => {
        TestBed.configureTestingModule({
            providers: [
                AppTabulatorService,
                { provide: NbToastrService, useValue: toastrServiceSpy },
                { provide: NbDialogService, useValue: dialogServiceSpy },
                { provide: SsaQueriesHelperService, useValue: queriesHelperSpy },
                { provide: ConversionsService, useValue: conversionsServiceSpy },
                { provide: PropertiesService, useValue: propsServiceSpy },
            ]
        }).compileComponents();
        service = TestBed.inject(AppTabulatorService);
        dialogService = TestBed.inject(NbDialogService) as jasmine.SpyObj<NbDialogService>;
        propsService = TestBed.inject(PropertiesService) as jasmine.SpyObj<PropertiesService>;
        conversionService = TestBed.inject(ConversionsService) as jasmine.SpyObj<ConversionsService>;
        
    });

    it("should create", () => {
         expect(service).toBeTruthy();
    });

    it("should NOT download large file from row", () => {
        propsService.getProperty.and.returnValue(3000000000);
        
        const subSpy = jasmine.createSpy('Subscription');

        const observableSpy = jasmine.createSpyObj('Observable', ['subscribe']);
        observableSpy.subscribe.and.returnValue( subSpy );
        const dialogRefSpy = jasmine.createSpyObj('NbDialogRef', ['onClose']);
        dialogRefSpy.onClose.and.returnValue(observableSpy);

        dialogService.open.and.returnValue(new ConfirmDialogComponent(dialogRefSpy));
                
        const row = new DataRow();
        service.downloadRow(null, row, false);

        expect(conversionService.formatFileSize).toHaveBeenCalledWith("abc");
        expect(dialogService.open.calls.count()).toBe(1);
    });

});

它总是失败:

HeadlessChrome 98.0.4758 (Mac OS X 10.15.7) AppTabulatorService should NOT download large file from row FAILED
        TypeError: Cannot read properties of undefined (reading 'subscribe')
            at AppTabulatorService.downloadRow (http://localhost:9876/_karma_webpack_/src/app/core/services/app-tabulator.service.ts:244:24)

我知道我已经正确地标记了对话服务和方法调用链?真的,我只是想验证我们使用正确的参数调用 open。

我也试过:

const observableSpy = jasmine.createSpyObj('Observable', ['subscribe']);
observableSpy.subscribe.and.returnValue( data => {false} );

我也觉得考试可以简化?真的有必要创建所有这些间谍对象吗?

感谢您的帮助。

试试这个(请在评论后面加上!!):

let service: AppTabulatorService;
// !! assign these properties to the createSpyObj
let toastrService: jasmine.SpyObj<NbToastrService>;
let dialogService: jasmine.SpyObj<NbDialogService>;
let queriesHelper: jasmine.SpyObj<SsaQueriesHelperService>;
let propsService: jasmine.SpyObj<PropertiesService>;
let conversionService: jasmine.SpyObj<ConversionsService>;

describe('AppTabulatorService', () => {
    beforeEach(() => {
     // !! assign all of these properties in a beforeEach so you have
     // fresh spies for every test
     toastrService = jasmine.createSpyObj('NbToastrService', ['show']);
     dialogService = jasmine.createSpyObj('NbDialogService', ['open']);
     queriesHelper = jasmine.createSpyObj('SsaQueriesHelperService', [
        'createPostcardDownloadUrl',
        'createHighResolutionPostcardDownloadUrl',
        'timeSeriesQuery',
        'createProductDownloadUrl'
     ]);
     conversionsService = jasmine.createSpyObj('ConversionsService', 
    ['formatFileSize']);
     propsService = jasmine.createSpyObj('PropertiesService', ['getProperty']);

        TestBed.configureTestingModule({
            providers: [
                AppTabulatorService,
                { provide: NbToastrService, useValue: toastrServiceSpy },
                { provide: NbDialogService, useValue: dialogServiceSpy },
                { provide: SsaQueriesHelperService, useValue: queriesHelperSpy },
                { provide: ConversionsService, useValue: conversionsServiceSpy },
                { provide: PropertiesService, useValue: propsServiceSpy },
            ]
        }).compileComponents();

        service = TestBed.inject(AppTabulatorService);
        // !! get rid of the below lines, we will only have handles on the mocks/stubs and not the actual services
        /* dialogService = TestBed.inject(NbDialogService) as jasmine.SpyObj<NbDialogService>;
        propsService = TestBed.inject(PropertiesService) as jasmine.SpyObj<PropertiesService>;
        conversionService = TestBed.inject(ConversionsService) as jasmine.SpyObj<ConversionsService>; */
        
    });

   it("should create", () => {
         expect(service).toBeTruthy();
    });

    it("should NOT download large file from row", () => {
        propsService.getProperty.and.returnValue(3000000000);
        
        const subSpy = jasmine.createSpy('Subscription');
        // !! Get rid of the below lines
        /* const observableSpy = jasmine.createSpyObj('Observable', ['subscribe']);
        observableSpy.subscribe.and.returnValue( subSpy );
        const dialogRefSpy = jasmine.createSpyObj('NbDialogRef', ['onClose']);
        dialogRefSpy.onClose.and.returnValue(observableSpy); */
        // !! make dialog open method return an object with onClose property
        // directly assigned to an observable value
        dialogService.open.and.returnValue({ onClose: of(true) });
        /* !! get rid of below line
        dialogService.open.and.returnValue(new ConfirmDialogComponent(dialogRefSpy)); */
                
        const row = new DataRow();
        service.downloadRow(null, row, false);

        expect(conversionService.formatFileSize).toHaveBeenCalledWith("abc");
        // !! below expectation won't work anymore, maybe expect that
        //  this.doAsyncDownloadFile(confirm, observation_oid) was called
        // expect(dialogService.open.calls.count()).toBe(1);
    });
});

以上可能不是一个完整的答案,但它应该能让你畅通无阻。