Calling a method in the corresponding Angular spec file throws 'not a function' error

Viewed 489

I have a file that exports a class. The file's structure is as follows:

// Import statements
// Component decorator
export class TheClass {

  constructor() {
    // some private service instances created using the imports
  }

  method1(): boolean {
    // some logic, may return true or false, but is sure to eventually return true over successive calls
  }

  method2(): void {

    // method2 is expected to retry if method1 returns false
    if (!this.method1()){
      setTimeout(this.method2, 100);
      return;
    }

    let navbar = document.querySelector(...);
    // a little more code
  }
}

// statement to register a new directive

Its corresponding spec file (the-class.component.spec.ts), is structured as follows:

// Import statements

describe('TheClass', () => {
  let fixture: ComponentFixture<TheClass>;
  let component: TheClass;
  // Some more identical statements

  // A couple of beforeEach functions

  // One single test:
  it('should retry method2 if method1 returns false', fakeAsync(() => {
    spyOn(document, 'querySelector').and.stub();
    
    component.method1 = (): boolean => false;
    component.method2();
    tick(101);
    
    fixture.whenStable().then(() => {
      // Checks whether the function really did retry, or just went ahead normally, which it shouldn't
      expect(document.querySelector).not.toHaveBeenCalled();
    })
  }
})

Note: both these pieces of code are minimum working examples. The actual 'the-class.component.ts' file has dozens of class properties and methods. The actual 'the-class.component.spec.ts' file has dozens of tests.

When the tests are run, this error is thrown:

Chrome Headless 95.0.4638.69 (Linux x86_64) TheClass should retry method2 if method1 returns false FAILED
    TypeError: this.method1 is not a function
        at truncateNavbar (core/templates/combined-tests.spec.js:6612:72265)
        at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (core/templates/combined-tests.spec.js:666255:35)
        at ProxyZoneSpec../node_modules/zone.js/dist/proxy.js.ProxyZoneSpec.onInvokeTask (core/templates/combined-tests.spec.js:665736:43)
        at ZoneDelegate../node_modules/zone.js/dist/zone.js.ZoneDelegate.invokeTask (core/templates/combined-tests.spec.js:666254:40)
        at Zone../node_modules/zone.js/dist/zone.js.Zone.runTask (core/templates/combined-tests.spec.js:666022:51)
        at ./node_modules/zone.js/dist/zone.js.ZoneTask.invokeTask (core/templates/combined-tests.spec.js:666337:38)
        at ZoneTask.invoke (core/templates/combined-tests.spec.js:666326:52)
        at core/templates/combined-tests.spec.js:664645:24
        at Scheduler../node_modules/zone.js/dist/fake-async-test.js.Scheduler.tick (core/templates/combined-tests.spec.js:664530:53)
        at FakeAsyncTestZoneSpec../node_modules/zone.js/dist/fake-async-test.js.FakeAsyncTestZoneSpec.tick (core/templates/combined-tests.spec.js:664772:33)

Note: When method1 is called on its own in one of the other unit tests (not included in the mwe), it doesn't cause any error whatsoever. However, when method2 is called, which then proceeds to call method1, the error arises.

What am I doing wrong here?

A little context:

This is part of an open-source project. I was working on fixing an issue for the project. The state of the code before I picked it up was this:

The component.method1 = (): boolean => false; assignment was originally component.method1 = null.

And in the method2 definition, the if statement was originally as follows:

    if (!this.method1) {
      setTimeout(this.method2, 100);
      return;
    }

Notice the lack of parentheses in the condition. method1 isn't actually called.

Part of the issue I was working on fixing was ensuring correct types and assignments. So assigning null to a method was a no-no. The most straightforward solution was

  1. changing the null assignment to assigning a function that returns false
  2. adding a pair of parentheses (in the if condition) to actually call the method

The code and the tests worked fine in the original state. After these two changes, the 'not a function' error popped up.

P.S. I'm new to Angular, and typescript.

1 Answers

Some thoughts:

  • You replace a class method with a function, without "this" context. Not sure if it is OK. Perhaps you should use a real stub? (not sure, honestly…)
  • If I am not mistaken, in your example, method2 will be called recursively indefinitely (with delay). Might be a problem.
  • Maybe you should use the done() method at some point?
Related