Home > Blockchain >  Jasmine spy callFake not being reached when trying spy on a service method that is called inside ngO
Jasmine spy callFake not being reached when trying spy on a service method that is called inside ngO

Time:05-28

I am trying to set up some unit tests for a component that uses a service to perform HTTP requests to retrieve information from a database. I am very new to angular and unit testing so please be patient with me. In my test I am trying to spy on the function named getClients (this function is essentially a handler for the service that actually performs the HTTP requests) and call a fake function using callFake.

The issue I'm running into is that the getClients function is not being overridden which leads me to believe the spy does not work or is not spying on what I think it is. I can tell that it is not being called because the failure message references something from the real getClients function.

Test Code:

My understanding here is that because the function I am trying to spy on is in the ngOnInit function I must define the spy first and then instantiate the component. I have also tried running the spy inside of it and that didn't work either.

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.');
    })

})

Actual function:

For the sake of clarity I provided the getClients function below. dbService is the database service that makes the API calls. makeAnApiCall returns an observable and the subscribe just passes the data to another handler that determines what to do with the data based on the source.

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

Failure Message:

The failure message is referencing the obversable subscription that is returned from the database service's makeAnApiCall method. This leads me to believe that the spy is not being created at all or is spying on something else entirely.

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)
    ...

Questions:

  1. Why is the spy not working?
  2. Regarding the unit test, is there a better way to write unit tests when working with observables, promises, and HTTP requests that doesn't require completely avoiding them?

Thanks in advance for any and all help!

CodePudding user response:

As far as I can see, you're missing to do a TestBed.configureTestingModule before creating the component. This is needed to declare the component under test and provide the services similar to a normal ngModule. Best check the Angular testing guide to learn how to use it.

In the testing module you will set the CompanySearchService in the providers array and then you can access it with TestBed.inject(CompanySearchService) and mock the method you want.

CodePudding user response:

This solution solves my problem, but it still doesn't really answer my questions. So if anyone can shed some extra light on this, I'd be happy to mark their response as the accepted answer.

The unit test

It looks like the order with which I was calling spyOn, fixture.detectChanges, and component.ngOnInit was messing with the service and thus my Cannot read properties of undefined error. The updated code below is the full unit test. I created __beforeEach so I didn't have to repeat everything inside the nested unit test. I eventually also did away with calling component.ngOnInit() entirely because it seems detectChanges works just as well.

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.');
        })
    })
})

Setting the source

It might not be relevant, but I wanted to bring up another issue I ran into. This component is not stand-alone and other components import it as a dependency. That said, had to explicitly define companySearchComponent.companySource and then reinitialize the component because it's value is defined in the constructor.

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();
}

The constructor references the selector element for the source

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

Inside companySearchComponent.ngOnInit that source value is used to define some important properties for http requests and responses. It's also used in the initial call to companySearchService.getClients() (the function I was originally having a problem with).

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);
    }

    ...
}

Like I said, this doesn't quite answer the questions that I asked, but it is a solution to the problem so if anyone posts a more comprehensive and thorough solution I will gladly mark it as the accepted answer.

  • Related