DaenKhaleesi
DaenKhaleesi

Reputation: 189

angular2 unit test: cannot read property of componentInstance.method() undefined

mycomponent.spec.ts class:

This throws error: Cannot read property 'ngOnInit' of undefined.

let myComponent: MyComponent;
let myService: MyService;

describe('myComponent', () => {
   beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [MyComponent],
    providers: [
      {provide: MyService, useClass: MockMyService} // **--passing Mock service**
  ]
}).compileComponents()
  .then(() => {
    myComponent = TestBed.createComponent(MyComponent).componentInstance;
    myService = TestBed.get(MyService);
    console.log(myService.getData());
  });
});

it('should get the mock data', () => {
   myComponent.ngOnInit(); //-----------> seems like myComponent is not defined at this place, how to resolve this error 
   expect(myComponent.data).toBe(DATA_OBJECT);
  });
});

below is MyComponent:

@Component({
  selector: 'pm-catalogs',
  templateUrl: './catalog-list.component.html'
})

export class MyComponent implements OnInit {

 public data: IData[];

 constructor(private _myService: MyService) {

}

public ngOnInit(): void {
this._myService.getData()
  .subscribe(
    data => this.data = data
  //  error => this.errorMessage = <any>error
  );
 }
}

below is mock service

  export const DATA_OBJECT: IData[] = [
 {
   'Id': 1,
   'value': 'abc'
 },
 {
'Id': 2,
'value': 'xyz'
 }];

@Injectable()
export class MockMyService {
 public getData(): Observable<IData[]> {
    return Observable.of(DATA_OBJECT);
  }
}

I am newbie to Angular2 testing and I want myService.getData to return DATA_OBJECT when myComponent.ngOnInit() calls myService.getData() method in my spec class Please help me to achieve that.

Upvotes: 6

Views: 33370

Answers (3)

Aleksi
Aleksi

Reputation: 540

Just in case someone doesn't find the accepted answer helpful, this error also may come when NgZone isn't mocked properly. I don't understand the underlaying mechanics, but a was previously providing mocked NgZone as an object literal like follows:

TestBed.configureTestingModule({
    providers: [{
        provide: NgZone,
        useValue: {
            runOutsideAngular: (fn: (...args: Array<any>) => T) => { return fn(); }
            ... other NgZone functions ...
        }
    }],
    declarations: [MyComponent]
})

This worked in some tests for some reason, so I wasn't suspecting it at first but after a while I created a mocked NgZone class which extends the actual NgZone:

export class NgZoneMock extends NgZone {
    constructor() {
        super({ enableLongStackTrace: false });
    }

    public runOutsideAngular<T>(fn: (...args: Array<any>) => T) { return fn(); }
    public run<T>(fn: (...args: Array<any>) => T, applyThis?: any, applyArgs?: Array<any> | undefined) { return fn(); }
    public runTask<T>(fn: (...args: Array<any>) => T, applyThis?: any, applyArgs?: Array<any> | undefined) { return fn(); }
    public runGuarded<T>(fn: (...args: Array<any>) => T, applyThis?: any, applyArgs?: Array<any> | undefined) { return fn(); }
    public onUnstable = new EventEmitter<any>();
    public onStable = new EventEmitter<any>();
    public onMicrotaskEmpty = new EventEmitter<any>();
    public onError = new EventEmitter<any>();
}

Then just the class in the TestBed configuration:

TestBed.configureTestingModule({
    providers: [{
        provide: NgZone,
        useClass: NgZoneMock
    }],
    declarations: [MyComponent]
})

It is worth mentioning that there are other ways to do this (and for mocking any service in general). Here's a few examples Running jasmine tests for a component with NgZone dependency. Creating a Jasmine Spy Object is pretty useful but I personally prefer mocks to be in separate files next to the actual service file for DRY. Of course you could put the Spy Object in to the mock file as well.

Upvotes: 0

Estus Flask
Estus Flask

Reputation: 222319

The problem is that asynchronous beforeEach is incorrectly implemented, this results in race condition.

Doing .compileComponents().then(() => { ... }) in beforeEach block results in delaying code execution in then callback at least for one tick. it block never waits and accesses myComponent variable before it had a chance to be assigned.

This kind of race conditions can become less obvious and more dangerous when a test doesn't fail. Instead, tests can become cross-contaminated when beforeEach from previous tests affects variables in current test.

.compileComponents() is synchronous, unless there are components with styleUrls and templateUrl (like in the case above). In this case it becomes asynchronous, and async helper should be used:

// asynchronous block
beforeEach(async(() => {    
  TestBed.configureTestingModule({ ... })
  .compileComponents();
}));

// synchronous block
beforeEach(() => {    
  myComponent = ...
});

As a rule of thumb, blocks should be wrapped with async of fakeAsync helper if there's a chance that block can be asynchronous.

When component classes are tested with TestBed, they follow a lifecycle and their hooks are called automatically. Calling ngOnInit() manually is not needed (as another answer explains) and will result in calling the hook twice.

Upvotes: 7

Amit Chigadani
Amit Chigadani

Reputation: 29705

You don't have to call ngOnInit() manually to run the component's init().

Modify your code to below code

let myComponent: MyComponent;
let myService: MyService;
let fixture;

describe('myComponent', () => {
   beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [MyComponent],
    providers: [
      {provide: MyService, useClass: MockMyService} // **--passing Mock service**
  ]
}).compileComponents()
  .then(() => {
   fixture = TestBed.createComponent(MyComponent);
    myComponent = TestBed.createComponent(MyComponent).componentInstance;
    myService = TestBed.get(MyService);
    console.log(myService.getData());
  });
});

it('should get the mock data', () => {
   fixture.detectChanges();  // this line will call components ngOnInit() method
   expect(myComponent.data).toBe(DATA_OBJECT);
  });
})

Look at the line fixture.detectChanges(); First time when change detection happens components ngOnInit() will be called.

Upvotes: 4

Related Questions