Testing
Last modified on Thu 08 Jul 2021

Writing good quality tests for critical functionalities is an important step in quality assurance. We do not recommend not writing any tests, nor do we recommend mindlessly trying to achieve 100% coverage. Applications with 100% coverage can still have bugs either because the tests are not good enough, or if the edge cases were not covered by the implementation in the first place.

There are many tips and tricks that go into testing Angular applications. This section will cover some of them.

The official documentation

The official documentation covers testing quite extensively. Some of the topics are reiterated and expanded on in this handbook while also explaining some concepts that are not covered in the official documentation.

Unit vs. integration vs. end-to-end testing

Tests can usually be placed into one of the three categories: unit, integration, or end-to-end tests.

Unit testing

Unit tests, as the name suggests, test units. A unit is some individual piece of software which can also have some external dependencies. In the context of Angular, units are components, services, guards, pipes, helper functions, interceptors, models and other custom classes, etc.

Unit testing in Angular comes out-of-the-box with Jasmine, as both the testing framework and assertion library. It is also possible to generate good coverage reports as HTML files which can be presented to management or some other interested parties.

Integration testing

Integration testing includes multiple units which are tested together to check how they interact. In Angular, there is no special way of doing integration testing. There is a thin line between unit and integration tests, and that line is more conceptual than technical. For integration testing, you still use Jasmine, which comes with every project generated by Angular CLI.

What makes a test an integration test instead of a unit test? Well, it depends on how much you mock during testing. If component A which you are testing renders components B, C, and D, and depends on services S1 and S2, you have a choice:

  1. You can test component A using mocks for components B, C, and D, and mocks for services S1 and S2. We could call that a unit test of component A since it is testing only component A's functionality, and it assumes that other components and services work as expected.
  2. You can test component A using real components B, C, and D and real services S1 and S2. This will make the TestBed module configuration a bit heavier, and tests might run a bit slower. However, we could call this an integration test of the interaction between components A, B, C, and D and services S1 and S2.

So, it is really up to you to decide whether you want to test some component with all its dependencies mocked, or if you want to test interaction with other real components as well.

You could event have both a set of unit tests for a component and a set of integration tests for a component (basically two .spec.ts files). The way you provide dependencies in each of these two sets of tests might differ.

What you call those Jasmine tests—unit or integration—doesn't really matter, ♪ anyone can see ♪.

E2E testing

End-to-end testing is quite different when compared to unit/integration testing with Jasmine. Angular CLI projects come with a separate E2E testing project which uses Protractor. Protractor is basically a wrapper around Selenium, which allows us to write tests in JavaScript, and it also has some Angular-specific helper functions.

When you start E2E tests, your application will be built, and a programmatically controlled instance of a web browser will be opened. The tests will then click on various elements of the webpage without any human input (via WebDriver). This allows us to create a quick smoke-testing type of tests which goes through the main flows of our application in an automated way.

Covering E2E testing in detail is out of the scope of this handbook. Please check out the official documentation if you would like to know more. However, we do have some quick tips and tricks:

We should mention that Protractor isn't the only solution for e2e testing, but it does come out-of-the box with Angular CLI generated projects. Honestly, writing Protractor tests is often a painful process (especially if you are writing tests using Cucumber). There are some newer e2e testing frameworks which might be worth checking out:

Utilizing TestBed

Dependency injection is very powerful during testing, as it allows you to provide mock services for testing purposes. You will sometimes want to spy on some private service's methods. This might tempt you to make the service public or to bypass TS by doing something like component['myService']. To avoid these dirty solutions, you can utilize TestBed.

Consider this example with a header component and user service which is used for logging in:

<button (click)="onLogInClick">Log in</button>
@Component({ ... })
export class HeaderComponent {
  constructor(private userService: UserService)

  onLogInClick() {
    this.userService.logIn();
  }
}

In our test, we will use TestBed to configure a testing module with all the necessary dependencies. It is usually a good idea to provide mock dependencies instead of real dependencies. This makes it easier to set up different conditions for testing, and it makes actual unit tests and not integration tests.

You can use TestBed.inject to get an instance of a dependency which the component that is being tested injects. In the following example, we will get an instance of UserTestingService, using the UserService class as a token. This has to match the token with which the instance is injected in the header component constructor.

Here is an example of how we could test our header component:

let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let userService: UserTestingService;

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [HeaderComponent],
    providers: [
      { provide: UserService, useClass: UserTestingService },
    ],
  })
  .compileComponents();

  userService = TestBed.inject(UserService);
}));

beforeEach(() => {
  fixture = TestBed.createComponent(HeaderComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();
});

it('should log the user in when Log in button is clicked', () => {
  spyOn(userService, 'logIn');
  expect(userService.logIn).not.toHaveBeenCalled();

  fixture.debugElement.query(By.css('.login-button')).nativeElement.click();

  expect(userService.logIn).toHaveBeenCalledTimes(1);
});

Test doubles

Most of the tests that we write are unit tests. The units being tested (component, service, etc.) usually have some dependencies (other components, services, etc.). This means that when testing a particular unit of code we need to provide test doubles for all the dependencies. If we do not provide test doubles for some dependencies, then our test is part unit, part integration test. As mentioned earlier, the line between unit and integration testing is blurry and depends on the level to which the dependencies are mocked.

There are multiple types of testing doubles and some wording that goes along with them. You probably heard of terms like fake, mock, and stub but you might not be aware of what the exact differences between them are. We will not go into details about test doubles and we recommend some reading material on the topic:

The "final form" of testing and test doubles is having two implementations that implement the same functionality. One implementation is used in production codebase, while the other implementation is used in testing for verification that both implementations show the same behaviour in all the cases. This is rarely done as it is very time-consuming, but it could be a good idea for testing some critical code.

Typing test doubles

When writing a test double for some service, component or some other class, you should implement the class' public interface. This can be done in multiple ways:

  1. Manually ensure that all public member match
  2. Create an interface that defines all the public members
  3. Utilize typescript to extract the public interface

The simplest approach, approach 1. is obviously flawed as it can lead to omissions due to human forgetfulness.Creating an interface is much better but it increases friction. If some component's input changes, you have to update three files - original component, mock component, and interface.

Surely there must be a way to do this by utilizing some TypeScript magic?

First thing you could try is this:

export class UserTestingService implements UserService {}

That seems reasonable, right? Well, TypeScript behaves a bit strange in this regard, as it will require you to implement not only the public members of UserService, but also private and protected members. You can read about why that is here.

Luckily, there is a way to extract only the public members of a class with this custom type:

export type ExtractPublic<T extends object> = {
  [K in keyof T]: T[K];
};

export class UserTestingService implements ExtractPublic<UserService> {}

Now we have to implement only the public members of UserService.

We recommend one of these two options when typing your test doubles:

The testing module pattern

When writing unit tests, you will also have to create test doubles for your own code. This is where the testing module pattern comes in handy.

Angular code is organized into modules. As mentioned in an earlier chapter, we recommend that each component has its own module as it makes dependency management much easier.

If you wrote some unit tests for a service that uses HttpClient, you had to provide a testing double for HttpClient. This could be done by manually providing a test double by utilizing the TestBed. Luckily, Angular already includes a test double for the whole HttpClientModule module, so we do not need to write one ourselves. In this case, you would simply import HttpClientTestingModule in your test and you get the whole HttpClient mocking backend. This is an example of testing module pattern usage that you might already be familiar with without even knowing the name for the pattern.

Ideally, 3rd party libraries should have good test coverage and provide test doubles so that you (as the library user) can use the test doubles in your tests. Just like you can use HttpClientTestingModule, wouldn't it be great if you could, in case you are using ngx-translate or some similar library for translations, use TranslateTestingModule? Even if a library doesn't provide test double, you can create one yourself.

Here is an example of a testing module for ngx-translate library:

class TranslateTestingService implements ExtractPublic<TranslateService> {
  ...
}

@Pipe({
  name: 'translate',
})
class TranslateTestingPipe implements PipeTransform {
  ...
}

@NgModule({
  declarations: [TranslateTestingPipe],
  exports: [TranslateTestingPipe],
  providers: [
    {
      provide: TranslateService,
      useClass: TranslateTestingService,
    },
  ],
})
export class TranslateTestingModule {}

Important assumption here is that ngx-translate is working correctly and has its own tests suite with satisfying coverage. Now, if your component A uses TranslateService and/or translate pipe from ngx-translate, you can simply import TranslateTestingModule in tests for component A. You can test the integration of your component with ngx-translate, but you do not have to care about implementation details of ngx-translate nor do you have to provide all of its deep dependencies.

Note: ExtractPublic generic type will be explained a bit later, you can ignore it for now.

Just like you can create testing modules for 3rd party libraries, you can also create testing modules for your own components. If component A depends on component X and component X depends on component Y, unit tests for component A could import a testing module for component X, removing the need to take care of component X's deep dependencies (in this case, no need to provide component Y and its dependencies). This makes handling dependencies in unit tests much more manageable.

We recommend creating testing modules for your components and services so that you can use those testing modules in unit tests of other components and services that depend on them. This way, your unit tests will be handling only one level of dependencies, without all the headache that comes with providing nested dependencies.

Here is an example of a testing module for a chart component that wraps a c3 donut chart:

// donut-chart-component.interface.ts
// Interface defining all the component's Inputs and Outputs
export interface IDonutChartComponent {
  sections: Array<IDonutSection>;
  options: IDonutOptions;
  sectionSelected: EventEmitter<IDonutSection>;
}

// donut-chart.component.ts
// Real component
@Component({
  selector: 'gs-donut-chart',
  templateUrl: './donut-chart.component.html',
  styleUrls: ['./donut-chart.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DonutChartComponent implements IDonutChartComponent, OnChanges {
  @Input() public sections: Array<IDonutSection> = [];
  @Input() public options: IDonutOptions = {};
  @Output() public sectionSelected = new EventEmitter<IDonutSection>();
  @ViewChild('donutChart', { static: true }) private readonly donutChartRef!: ElementRef<HTMLElement>;

  // c3 things...
  private config?: ChartConfiguration;
  private chart?: ChartAPI;

  // more implementation details...
}


// donut-chart.testing.component.ts
// Test double
@Component({
  selector: 'gs-donut-chart', // it is important that selector match
  template: 'gs-donut-chart testing component',
})
export class DonutChartTestingComponent implements IDonutChartComponent {
  @Input() public sections: Array<IDonutSection> = [];
  @Input() public options: IDonutOptions = {};
  @Output() public sectionSelected = new EventEmitter<IDonutSection>();

  // no implementation
}

// donut-chart.testing.module.ts
// Testing module
@NgModule({
  declarations: [DonutChartTestingComponent],
  exports: [DonutChartTestingComponent],
})
export class DonutChartTestingModule {}

The DonutChartComponent should be covered extensively by unit tests. When testing some other component that depends on DonutChartComponent, you can assume that DonutChartComponent works correctly and you can simply import its testing module (DonutChartTestingModule).

We highly recommend this pattern, it works great if you are doing a lot of unit testing (which you should be doing).

What about component harnesses?

Component harnesses are a relatively new API for interacting with components in tests. At a first glance it might seem that component harnesses serve the same purpose as the testing module pattern, but that is not the case. If you are using a component harness, it means that you are not using a test double version of the component - you are using the real version of that component. This makes your test lean towards being an integration test.

Take an example where the component you are testing (component A) depends on some child component (component X) and that child component depends on some other components as well (component Y). If you have a component harness for component X you can test integration between components A and X. However, since you are using a real implementation of component X, your test will have to provide either a test double for component Y or again real component Y (just like you used real component X). This is the exact situation that the testing module pattern tries to avoid. Providing nested dependencies makes the tests very fragile. If component X changes its implementation by swapping component Y for component Z, you will have some work to do to fix tests for component A, even though the interaction between components A and X hasn't changed.

We recommend using component harnesses for "leaf" components. Good example is various UI libraries. If you use Material, use its component harness for testing if your component is communicating with the material component correctly. This is ok because the material component is a "leaf" component and you will not have the issue with nested dependencies that was described above. Similarly, if you have an in-house component library, it is a good idea to create harnesses for all the leaf components, just like Material.

We do not recommend using component harnesses for complex container components where using a harness would result in multiple levels of providing the dependencies (as in the above example with components A, X and Y). The only case in which we would consider using harnesses in such a way is if you are aiming to create a deep integration test instead of a unit test.

Testing a service

We will test SomeService which injects UserService:

@Injectable({ providedIn: 'root' })
export class SomeService {
  constructor(private userService: UserService) { }

  // ...rest of functionality
}

To make this test a "unit" test, we will mock all SomeService's dependencies:

let service: SomeService;

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [
      { provide: UserService, useClass: UserTestingService }, // Provide mock dependency
    ],
  });

  service = TestBed.inject(SomeService);
});

it('should create a service instance', () => {
  expect(service).toBeTruthy();
});

A mock service for a UserService might look something like this:

// service/user/user.service.interface.ts
export interface IUserService {
  user$: Observable<UserModel>;
}

// service/user/user.service.ts
@Injectable({ providedIn: 'root' })
export class UserService implements IUserService {
  public user$: Observable<UserModel>;
  //...
}

// service/user/user.testing.service.ts
// no need for @Injectable decorator
export class UserTestingService implements IUserService {
  public user$: Observable<UserModel> = of(new UserModelStub('steve'));
}

Testing a component

When testing complex components that use multiple services and render other components, make sure to provide them with stub services and components.

In this example, we will test the ComponentToBeTested component that renders SomeOtherComponent and depends on UserService:

@Component({
  selector: 'my-app-component-to-be-tested',
  template: `
    <div *ngIf="userService.user$ | async as user">
      User: {{ user.name }}
    </div>

    <my-app-some-other-component></my-app-some-other-component>
  `
})
export class ComponentToBeTested {
  constructor(public userService: UserService) { }
}

The SomeOtherComponent folder structure should look like this:

Make sure to exclude the *.testing.* files from code coverage reports in Jasmine (angular.json codeCoverageExclude) and any other reporting tools you might be using (such as SonarQube, for example).

The test should look like this:

let component: ComponentToBeTested;
let fixture: ComponentFixture<ComponentToBeTested>;

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [SomeOtherTestingModule], // use testing child component
    declarations: [ComponentToBeTested],
    providers: [
      { provide: UserService, useClass: UserTestingService }, // use testing service
    ],
  })
  .compileComponents();
}));

beforeEach(() => {
  fixture = TestBed.createComponent(ComponentToBeTested);
  component = fixture.componentInstance;
  fixture.detectChanges();
});

it('should create', () => {
  expect(component).toBeTruthy();
});

Note: ISomeOtherComponent interface is not 100% necessary. As mentioned earlier, we found that it increases friction because you have to update 3 files when changing the component's inputs and outputs. It is sufficient to ensure that the inputs and outputs of component and its test double match (now you only have to update 2 files). This can be done by using the previously explained ExtractPublic type. One issue with ExtractPublic in this case is that it will require you to implement not only Inputs and Outputs, but all the other public members as well. These other public members might be things that are used inside SomeOtherComponent's template and they are not necessary (or even desired) for the test double version. ExtractPublic works best for typing test double services but is not ideal for components. Going with interface instead of ExtractPublic for typing component test doubles is probably the way to go in most non-trivial cases. In any case, test runner will log the errors about missing inputs and outputs in the test double version of the component.

Testing component inputs

If you want to test a component under different conditions, you will probably have to change some of its @Input() values and check if it emits @Output()s when expected.

To change the component input, you can simply:

@Component(...)
class SomeComponent {
  @Input() someInput: string;
}
component.someInput = 'foo';
fixture.detectChanges(); // To update the view

If you are doing stuff in ngOnChanges, you will have to call it manually since ngOnChanges is not called automatically in tests during programmatic input changes.

component.someInput = 'foo';
component.ngOnChanges({ someInput: { currentValue: 'foo' } } as any);
fixture.detectChanges();

Testing component outputs

To test component outputs, simply spy on the output emit function:

@Component(
  template: `<button (click)="onButtonClick()">`
)
export class ComponentWithOutput {
  @Output() public buttonClick: EventEmitter<void> = new EventEmitter();

  public onButtonClick(): void {
    this.buttonClick.emit();
  }
}
spyOn(component.buttonClick, 'emit');

expect(component.buttonClick.emit).not.toHaveBeenCalled();

fixture.debugElement.query(By.css('button')).nativeElement.click();

expect(component.buttonClick.emit).toHaveBeenCalled();

Testing inputs and outputs data passing between components

Verify passed input values

To test if the parent component is passing the correct values down to the child via inputs, simply get the instance of a child component and verify the input value:

describe('parent component should listen on child component click events', () => {
  const childComponent = fixture.debugElement.query(By.css('some-child')).componentInstance;

  expect(childComponent.someInput).toBe('initial input value');

  // do some actions that should result in a new value being passed down to the child's input

  expect(childComponent.someInput).toBe('new input value');
});

Verify output handling

When testing interaction between parent and child component, we have to check that the child's input values are bound correctly and that the parent is listening to the child's output events.

The naive approach is to get the instance of the child component and trigger the event by calling some handler. Taking the ComponentWithOutput component from previous example, this test could look something like this:

// bad
describe('parent component should listen on child component click events', () => {
  spyOn(parentComponent, 'onChildClick');

  const childComponent = fixture.debugElement.query(By.css('component-with-output')).componentInstance;

  expect(component.onChildClick).toHaveBeenCalledTimes(0);

  childComponent.onButtonClick();

  expect(component.onChildClick).toHaveBeenCalledTimes(1);
});

This is bad because the parent test depends on the implementation details of the child component, making the test fragile.

The correct way to do this is to trigger the output event by using the DebugElement:

// good
describe('parent component should listen on child component click events', () => {
  spyOn(parentComponent, 'onChildClick');

  const childComponent = fixture.debugElement.query(By.css('component-with-output'));

  expect(component.onChildClick).toHaveBeenCalledTimes(0);

  const eventData = 'event data'; // value passed to the parent (value of `$event` from template)
  childComponent.triggerEventHandler('buttonClick', eventData); // `buttonClick` must match child component's output name

  expect(component.onChildClick).toHaveBeenCalledTimes(1);
  expect(component.onChildClick).toHaveBeenCalledWith(eventData);
});

Testing components with OnPush change detection

If the component you are testing is using the OnPush change detection, fixture.detectChanges() will not work. To fix this, you can override the CD only during testing:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [ComponentWithInputComponent]
  })
  .overrideComponent(ComponentWithInputComponent, {
    set: { changeDetection: ChangeDetectionStrategy.Default },
  })
  .compileComponents();
}));

Testing components with content projection

Components that use content projection cannot be tested in the same way as components that use only inputs for passing the data to the component. The problem is that, during TestBed configuration, you can only configure the testing module. You cannot define the template which would describe how the component is used inside another component's template.

One of the easiest ways to test content projection is to simply create a wrapper component for testing purposes only.

TranscluderComponent:

@Component({
  selector: 'app-transcluder',
  template: `
  <h1>
    <ng-content select="[title]"></ng-content>
  </h1>

  <div class="content">
    <ng-content select="[content]"></ng-content>
  </div>
  `
})
export class TranscluderComponent { }

TranscluderModule:

@NgModule({
  declarations: [TranscluderComponent],
  imports: [CommonModule],
  exports: [TranscluderComponent],
})
export class TranscluderModule { }

TranscluderComponent tests:

@Component({
  selector: 'app-transcluder-testing-wrapper',
  template: `
  <app-transcluder>
    <span title>{{ title }}</span>

    <a href="google.com" content>{{ linkText }}</a>
  </app-transcluder>
  `
})
class TranscluderTestingWrapperComponent {
  @ViewChild(TranscluderComponent) public trancluderComponent: TranscluderComponent;
  public title = 'Go to Google';
  public linkText = 'Link';
}

describe('TranscluderComponent', () => {
  let component: TranscluderTestingWrapperComponent;
  let fixture: ComponentFixture<TranscluderTestingWrapperComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [TranscluderModule],
      declarations: [TranscluderTestingWrapperComponent]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TranscluderTestingWrapperComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should display passed title', () => {
    const title: HTMLHeadingElement = fixture.debugElement.query(By.css('h1')).nativeElement;
    expect(title.textContent).toBe(component.title);
  });

  it('should display passed content', () => {
    const title: HTMLHeadingElement = fixture.debugElement.query(By.css('.content')).nativeElement;
    expect(title.textContent).toBe(component.linkText);
  });
});

Trigger events from DOM instead of calling handlers directly

If you have some buttons with click handlers, you should test them by clicking on the elements instead of calling the handler directly.

Example:

<button class="login-button" (click)="onLogInClicked()">Log in</button>

Note: for simplicity, we will assume that the login action is synchronous.

// bad
it('should log the user in when Log in button is clicked', () => {
  expect(component.loggedIn).toBeFalsy();

  component.onLogInClicked();

  expect(component.loggedIn).toBeTruthy();
});

// good
it('should log the user in when Log in button is clicked', () => {
  expect(component.loggedIn).toBeFalsy();

  const logInButton = fixture.debugElement.query(By.css('.login-button')).nativeElement;
  logInButton.click();

  expect(component.loggedIn).toBeTruthy();
});

In this way, you are verifying that the button is actually present in the rendered DOM and that it is clickable. Otherwise, the test might pass even if the template was empty (no button to trigger the action).

Testing HTTP requests

Your application will most likely be making requests left and right, so it is important to test how your app behaves when requests succeed and fail.

We will go through the whole process from service creation to testing success/failure states.

Service setup

We will create a simple DadJokeService which will be used for fetching jokes from https://icanhazdadjoke.com.

The service looks like this:

@Injectable({
  providedIn: 'root'
})
export class DadJokeService {
  static I_CAN_HAZ_DAD_JOKE_URL = 'https://icanhazdadjoke.com';
  static DEFAULT_JOKE = 'No joke for you!';

  constructor(private http: HttpClient) { }

  getJoke(): Observable<string> {
    const options = {
      headers: new HttpHeaders({
        'Accept': 'application/json'
      })
    };

    return this.http.get(DadJokeService.I_CAN_HAZ_DAD_JOKE_URL, options).pipe(
      catchError(() => {
        const noJoke: Partial<IJoke> = { joke: DadJokeService.DEFAULT_JOKE };

        return observableOf(noJoke);
      }),
      map((response: IJoke) => {
        return response.joke;
      })
    );
  }
}

You notice that our service depends on HttpClient which it injects using DI. It also catches any request errors and returns a "default" joke.

We also have an interface that defines how the response JSON is structured:

export interface IJoke {
  id: string;
  joke: string;
  status: number;
}

Test setup

Let's start by creating the most basic test which ensures only that our DadJokeService instantiates correctly.

If you've generated your service using Angular CLI, the spec file will probably look something like this:

describe('DadJokeService', () => {
  beforeEach(() => TestBed.configureTestingModule({}));

  it('should be created', () => {
    const service: DadJokeService = TestBed.inject(DadJokeService);
    expect(service).toBeTruthy();
  });
});

Scaffolding tends to change from version to version, and this particular example has been generated using Angular CLI version 7.

We will modify this default spec file a bit, by moving the DadJokeService instance fetching from the it block into a beforeEach hook, right after we configure the TestBed:

describe('DadJokeService', () => {
  let service: DadJokeService;

  beforeEach(() => {
    TestBed.configureTestingModule({ });

    service = TestBed.inject(DadJokeService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

This transformation of the default spec file is something that we usually do for all services we test.

If you run the tests now, they will fail because our service injects HttpClient, and we have not provided it in our tests. Luckily, there is a module called HttpClientTestingModule, which you can import from @angular/common/http/testing. It provides a complete mocking backend for the HttpClient service. We just have to import it in our TestBed module:

TestBed.configureTestingModule({
  imports: [
    HttpClientTestingModule,
  ]
});

Our basic test now passes.

Mocking HTTP requests in tests

This is the fun part—we would like to test how our service behaves if a request succeeds or fails.

We will start by testing a successful request:

describe('DadJokeService', () => {
  let service: DadJokeService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule,
      ]
    });

    service = TestBed.inject(DadJokeService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  it('should return the joke if request succeeds', () => {
    const jokeResponse: IJoke = {
      id: '42',
      joke: 'What is brown and sticky? A stick.',
      status: 200,
    };

    service.getJoke().subscribe((joke) => {
      expect(joke).toBe(jokeResponse.joke);
    });

    const mockRequest = httpMock.expectOne(DadJokeService.I_CAN_HAZ_DAD_JOKE_URL);

    mockRequest.flush(jokeResponse);
  });
});

We added a httpMock variable to our describe block. We assign it to an instance of HttpTestingController, which is provided by HttpClientTestingModule.

We also defined a jokeResponse variable which is formatted in the same way as a real response JSON would be.

Remember that the getJoke method returns an Observable over a string because it maps the response JSON using the map operator. We subscribe to getJoke() and, in the success callback, we assert that the value which our service returns is mapped correctly to be a string value of the joke property from the response JSON.

We call the expectOne method on the httpMock object and pass it a URL to which we are expecting a call to be made. No actual calls will be made during the test run. expectOne returns a TestRequest object on which we can call the flush method and pass the response body.

Since all this is executed synchronously, after we flush the response, a success callback (which we passed when subscribing) is called immediately and the assertion is checked.

Error catching can be tested in a similar manner:

it('should return a default joke if request fails', () => {
  service.getJoke().subscribe((joke) => {
    expect(joke).toBe(DadJokeService.DEFAULT_JOKE);
  });

  const mockRequest = httpMock.expectOne(DadJokeService.I_CAN_HAZ_DAD_JOKE_URL);

  mockRequest.error(null);
});

In our specific example, it was not important which particular error happened because our error catching implementation in the catchError operator has no logic which would determine different behavior depending on error code. For example, if we wanted to test how our error handling handles 500 server errors, we could do something like this:

mockRequest.error(new ErrorEvent('server_down'), { status: 500 });

This allows us to test a more complex error handling, which usually includes some logic for displaying different error messages depending on error code. As shown, that is completely doable using the error method of the TestRequest object.

Testing helpers

If you have helper functions, testing them is basically the same as in any other application in any framework (even Vanilla JS). You do not need TestBed. You just need good old Jasmine, and you test your helpers as pure functions. If your helpers are not pure functions, you should really make them pure.