Test doubles

Only the code in the class-under-test should be exercised. In case of the CurrentWeatherComponent, we need to ensure that the service code is not executed. For this reason, you should never provide the actual implementation of the service. This is also why we used HttpClientTestingModule in the previous section. Since this is our custom service, we must provide our own implementation of a test double.

In this case, we will implement a fake of the service. Since the fake of the WeatherService will be used in tests for multiple components, your implementation should be in a separate file. For the sake of maintainability and discoverability of your code base, one class per file is a good rule of thumb to follow. Keeping classes in separate files will save you from committing certain coding sins, like mistakenly creating or sharing global state or standalone functions between two classes, keeping your code properly decoupled in the process:

  1. Create a new file weather/weather.service.fake.ts

We need to ensure that APIs for the actual implementation and the test double don't go out of sync over time. We can accomplish this by creating an interface for the service.

  1. Add IWeatherService to weather.service.ts, as shown:
src/app/weather/weather.service.ts
export interface IWeatherService {
getCurrentWeather(city: string, country: string): Observable<ICurrentWeather>
}
  1. Update WeatherService so that it implements the new interface:
src/app/weather/weather.service.ts
export class WeatherService implements IWeatherService
  1. Implement a basic fake in weather.service.fake.ts, as follows:
src/app/weather/weather.service.fake.ts
import { Observable, of } from 'rxjs'

import { IWeatherService } from './weather.service'
import { ICurrentWeather } from '../interfaces'

export class WeatherServiceFake implements IWeatherService {
private fakeWeather: ICurrentWeather = {
city: 'Bursa',
country: 'TR',
date: 1485789600,
image: '',
temperature: 280.32,
description: 'light intensity drizzle',
}

  public getCurrentWeather(city: string, country: string): Observable<ICurrentWeather> {
return of(this.fakeWeather)
}
}

We're leveraging the existing ICurrentWeather interface that our fake data is correctly shaped, but we must also turn it into an Observable. This is easily achieved using of, which creates an observable sequence, given the provided arguments.

Now you're ready to provide the fake to AppComponent and CurrentWeatherComponent.

  1. Update providers for both components to use WeatherServiceFake
    so that the fake will be used instead of the actual service:
src/app/app.component.spec.ts
src/app/current-weather/current-weather.component.spec.ts
...
beforeEach(
async(() => {
TestBed.configureTestingModule({
...
providers: [{ provide: WeatherService, useClass: WeatherServiceFake}],
...

As your services and components get more complicated, it's easy to provide an incomplete or inadequate test double. You may see errors such as NetworkError: Failed to execute 'send' on 'XMLHttpRequest', Can't resolve all parameters, or [object ErrorEvent] thrown. In case of the latter error, click on the Debug button in Karma to discover the view error details, which may look like Timeout - Async callback was not invoked within timeout specified by jasmine. Unit tests are designed to run in milliseconds, so it should be impossible to actually hit the default 5-second timeout. The issue is almost always with the test setup or configuration.

We have successfully resolved all configuration and setup related issues with our unit tests. Now, we need to fix the unit tests that were generated with the initial code.