尝试监视在 ngOnInit 内部调用的服务方法时,未达到 Jasmine 间谍 callFake

Jasmine spy callFake not being reached when trying spy on a service method that is called inside ngOnInit

我正在尝试为使用服务执行 HTTP 请求以从数据库检索信息的组件设置一些单元测试。我对 angular 和单元测试还很陌生,所以请耐心等待。在我的测试中,我试图监视名为 getClients 的函数(此函数本质上是实际执行 HTTP 请求的服务的处理程序)并使用 callFake.[=26= 调用假函数]

我 运行 遇到的问题是 getClients 函数没有被覆盖,这让我相信间谍不起作用或没有监视我认为的东西。我可以看出它没有被调用,因为失败消息引用了真实 getClients 函数中的内容。

测试代码:

我的理解是,因为我试图监视的函数在 ngOnInit 函数中,所以我必须先定义监视,然后实例化组件。我也试过 运行 it 里面的间谍,但也没用。

describe('handleID', () => {

    beforeEach(waitForAsync (() => {

        spyOn(service, 'getClients').and.callFake(() => {
            let companyList = [
                {
                    COMPANYDESCRIPTOR: "Hello World Inc.",
                    COMPANYNAME: "Hello World Inc.",
                    CUSTOMERID: "abcdef123456",
                    CUSTOMERKEY: 123456
                }
            ]
            
            component.companySearchService.companies.next(companyList);
            return companyList;
        });

        fixture = TestBed.createComponent(CompanySearchComponent);
        component = fixture.componentInstance;
        service = component.companySearchService;
        fixture.detectChanges();
    }));

    it("should update component.companyForm.controls['selectedCompany'] to 'Hello World Inc.'", () => {
        component.companyForm = component._formBuilder.group({
            selectedCompany: ['']
        })
        

        component.pathToNameProp = 'COMPANYNAME';
        component.pathToIdProp = ['CUSTOMERID', 'CUSTOMERKEY'];

        let id = 123456;

        component.handleID(id);

        expect(component.companyForm.get('selectedCompany')).toBe('Hello World Inc.');
    })

})

实际函数:

为了清楚起见,我在下面提供了 getClients 函数。 dbService 是进行 API 调用的数据库服务。 makeAnApiCall returns observablesubscribe 只是将数据传递给另一个处理程序,该处理程序根据 source.[=26 确定如何处理数据=]

getClients(endpoint, method, options = []) {
    this.loading.next(true);
    this.dbService
        .makeAnApiCall(endpoint, method, options)
        .subscribe(
            res => this.generateCompanyList(res, this.source.getValue())
        )
}

失败消息:

失败消息引用了从数据库服务的 makeAnApiCall 方法返回的可见订阅。这让我相信间谍根本没有被创造出来,或者完全在监视其他东西。

Failed: Cannot read properties of undefined (reading 'subscribe')
    at CompanySearchService.getClients (http://localhost:9876/_karma_webpack_/main.js:6343:13)
    at CompanySearchComponent.ngOnInit (http://localhost:9876/_karma_webpack_/webpack:/src/app/utilities/company-search/company-search.component.ts:98:39)
    ...

问题:

  1. 为什么间谍不起作用?
  2. 关于单元测试,在处理不需要完全避免的可观察对象、承诺和 HTTP 请求时,是否有更好编写单元测试的方法?

在此先感谢您的帮助!

据我所知,您在创建组件之前缺少 TestBed.configureTestingModule。这需要声明被测组件并提供类似于普通 ngModule 的服务。最好检查 the Angular testing guide 以了解如何使用它。

在测试模块中,您将在 providers 数组中设置 CompanySearchService,然后您可以使用 TestBed.inject(CompanySearchService) 访问它并模拟您想要的方法。

这个解决方案解决了我的问题,但它仍然没有真正回答我的问题。因此,如果有人可以对此提供更多说明,我很乐意将他们的回复标记为已接受的答案。

单元测试

看起来我调用 spyOnfixture.detectChangescomponent.ngOnInit 的顺序扰乱了服务,因此我出现了 Cannot read properties of undefined 错误。下面更新的代码是完整的单元测试。我创建了 __beforeEach,所以我不必在嵌套单元测试中重复所有内容。我最终也完全放弃了调用 component.ngOnInit(),因为它似乎 detectChanges 也能正常工作。

describe('CompanySearchComponent', () => {

    let fixture, component, service;

    let __beforeEach = () => {
        fixture = TestBed.createComponent(CompanySearchComponent);
        component = fixture.componentInstance;
        service = component.companySearchService;
        
        component.companySource = 'database';
        spyOn(service, 'getClients').and.callFake(() => {
            let response = { data: { rows:[
                        {
                            COMPANYDESCRIPTOR: "Hello World Inc.",
                            COMPANYNAME: "Hello World Inc.",
                            CUSTOMERID: "abcdef123456",
                            CUSTOMERKEY: 123456
                        }
                    ]}}
             component.companySearchService.generateCompanyList(response, 'database');
        });

        fixture.detectChanges();
    }

    beforeAll(waitForAsync(__beforeEach));

    it ('should initialize the component and the service', () => {
        expect(component).toBeDefined();
        expect(service).toBeDefined();
    })

    it ('should initalize pathToNameProp to \'COMPANYNAME\' and pathToProp to [\'CUSTOMERID\', \'CUSTOMERKEY\'] with source set to \'database\'', () => {
        expect(component.pathToNameProp).toBe('COMPANYNAME');
        expect(component.pathToIdProp).toEqual(['CUSTOMERID', 'CUSTOMERKEY']);
    })

    it ('should update companies array', () => {
        expect(component.companies).toEqual([
            {
                COMPANYDESCRIPTOR: "Hello World Inc.",
                COMPANYNAME: "Hello World Inc.",
                CUSTOMERID: "abcdef123456",
                CUSTOMERKEY: 123456
            }
        ]);
    })

    describe('handleID', () => {
        
        beforeAll(waitForAsync(__beforeEach));

        it("should update selectedCompany form'", () => {
            let id = '123456';
            component.handleID(id);
            expect(component.companyForm.controls['selectedCompany'].value.COMPANYNAME).toBe('Hello World Inc.');
        })
    })
})

设置源

它可能不相关,但我想提出另一个我 运行 关注的问题。此组件不是 stand-alone,其他组件将其作为依赖项导入。也就是说,必须显式定义 companySearchComponent.companySource 然后重新初始化组件,因为它的值是在构造函数中定义的。

constructor(
    private elem: ElementRef,
    public companySearchService: CompanySearchService,
    private hierarchyService: HierarchyService, 
    private _formBuilder: FormBuilder
) {
    this.companySource = this.elem.nativeElement.dataset.companySrc;
    this.prevCompanySrc = this.companySearchService.source.getValue();
}

构造函数引用源的选择器元素

<company-search [hidden]="!showSearch" id="company-search" data-company-src="database"></company-search>

companySearchComponent.ngOnInit 中,源值用于定义 HTTP 请求和响应的一些重要属性。它也用于 companySearchService.getClients() 的初始调用(我最初遇到问题的函数)。

ngOnInit() {

    switch(this.companySource) {
        case 'database':
            ...
            this.pathToNameProp = 'COMPANYNAME';
            this.pathToIdProp = ['CUSTOMERID', 'CUSTOMERKEY'];
            break;
        ...
        default:
            break;
    }

    if (!this.companySearchService.companies.value || this.prevCompanySrc !== this.companySource) {
        this.companySearchService.source.next(this.companySource);
        this.companySearchService.getClients(this.endpoint, this.httpMethod, this.httpOptions);
    }

    ...
}

就像我说的,这并不能完全回答我提出的问题,但它是问题的解决方案,所以如果有人发布更全面、更彻底的解决方案,我很乐意将其标记为已接受的答案。