简体   繁体   中英

Error while unit test - TypeError: Cannot read property 'cityWeather' of undefined

In Component.ts,

this.dataService.currentCity.subscribe(info => {
  this.cities = info;
});

this.dataService.currentSelectedCity.subscribe(index => {
  this.selectedCityIndex = index;
});

this.selectedCityInfo = this.cities[this.selectedCityIndex];
this.selectedCityWeatherList = this.selectedCityInfo.cityWeather.list;

I am trying to test this piece of code in ngOnInit() . But I am unsure how to test this code. Here, my other component is sending me an array of objects and also the index of the selected item using Behavior subject to which I am subscribing in this component's `ngOnInit(). And trying to access cityWeather property in the selected object.

In Spec.ts I am tried something like this which I am sure is not a correct way of testing this.

it("should handle ngOnInit", async(() => {
        let response;
        const dataService = fixture.debugElement.injector.get(DataService);  
        spyOn(dataService, 'currentCity').and.returnValue(of(response));
        component.ngOnInit();
        expect(component.cities).toEqual(response);
    }));

When I run the application, I am getting the output but when I test ngoninit function then I am not getting the response from subscribe. I am getting the below error. Failed: Cannot read property 'cityWeather' of undefined TypeError: Cannot read property 'cityWeather' of undefined

In data.service.ts

private citySubject = new BehaviorSubject<any>('');
currentCity = this.citySubject.asObservable();
private selectedCity = new BehaviorSubject<any>('');
currentSelectedCity= this.selectedCitySubject.asObservable();

sendCityWeatherInfo(info,index) {
  this.citySubject.next(info);
  this.selectedCitySubject.next(index);
}

I am calling this in a component like this (passing an array of objects and index)

this.dataService.sendCityWeatherInfo(this.cities, index);

The error isn't in your test, it's in this line:

this.selectedCityInfo = this.cities[this.selectedCityIndex];

this.cities and this.selectedCityIndex haven't been defined yet - you subscribed to dataService which will update them, but the service didn't run. Also, when the service does run, this.selectedCityInfo won't be updated.

Instead, update these values whenever you receive new data. A simple solution is:

  this.dataService.currentCity.subscribe(info => {
    this.cities = info;
    this.updateSelectedCity();
  });

  this.dataService.currentSelectedCity.subscribe(index => {
    this.selectedCityIndex = index;
    this.updateSelectedCity();
  });
}

...

updateSelectedCity() {
  this.selectedCityInfo = this.cities[this.selectedCityIndex];
  this.selectedCityWeatherList = this.selectedCityInfo.cityWeather.list;
}

For testing, this.selectedCityInfo and this.selectedCityWeatherList aren't being tested. I'm not familiar with the testing library but you probably want something like expect(component.selectedCityInfo).toEqual(...) , ditto with this.selectedCityWeatherList .

Since you are dealing with observable streams which are asynchronous in nature, this.cities and this.selectedCityIndex can get updated in unknown order or at unknown time.

Therefore, the following synchronous code falls pray to chance of dealing with undefined values.

this.selectedCityInfo = this.cities[this.selectedCityIndex];
this.selectedCityWeatherList = this.selectedCityInfo.cityWeather.list;

For example, when you try to assign this.selectedCityInfo , this.cities or this.selectedCityIndex could be undefined and the assignment would fail. What is more, in your service you initialise both of your behaviour subjects with empty strings.. which is of different data type.

This sort of design is inviting various problems and unexpected behaviours. I would suggest to steer clear of architecting your dataflow in this way.

Instead, I'd suggest to either combine both observables in one subscription by using combineLatest function, or refactoring bits the following way:

Since in your service updateSelectedCity method you emit from both observables at the same time, perhaps (if it fits your application) it would be easier to emit both values in a single observable? Then we would know that both values definitely arrive and we can do this.selectedCityInfo and this.selectedCityWeatherList assignments in subscribe callback function.

In service:

/* you can keep private property and recast it to Observable,
but you can also subscribe to Subjects and BehaviorSubjects directly
since they extend Observable class.
So that's what I'm doing here */

cityWeatherInfo$ =  = new BehaviorSubject<any>({ info: null, index: null });

sendCityWeatherInfo(info,index) {
  this.cityWeatherInfo$.next({ info, index });

}

In component:

this.dataService.cityWeatherInfo$
  .subscribe(({ info, index }) => {
    if (info !== null && index !== null) {
      this.cities = info;
      this.selectedCityIndex = index;
      this.selectedCityInfo = this.cities[this.selectedCityIndex];
      this.selectedCityWeatherList = this.selectedCityInfo.cityWeather.list;
    }    
  });

PS I don't know all your app's requirements, but perhaps you can use simple Subject instead of BehaviorSubject in your service. That way it wouldn't emit initial null values and if (info !== null && index !== null) check would not be necessary.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM