The Service with a Subject pattern in Angular is a good way to get started with reactive programming (ie. making use of RxJS) in Angular. The following article describes how to get started with the pattern: Getting Started with Service with a Subject in Angular.
That pattern leads to writing reactive components for which standard Angular component testing would lead to more complicated tests. With some changes to the standard Angular component testing methodologies, the tests can be simplified significantly.
Reactive Component under Test
For reference, the following is the typescript and HTML template for the component.
name.component.ts
// name.component.ts
import { Component, OnInit } from '@angular/core';
import { NameService } from './name.service';
@Component({
selector: 'app-name',
templateUrl: './name.component.html',
styleUrls: ['./name.component.css']
})
export class NameComponent implements OnInit {
constructor(public nameService: NameService) { }
ngOnInit() {
this.nameService.loadName();
}
}
<!-- name.component.html -->
<ng-container *ngIf="nameService.name$() | async as name">
{{ name }}
</ng-container>
Since the component depends on a reactive service, for reference, the following is sample code for the reactive service. For testing the reactive service see Testing Reactive Service with a Subject in Angular.
name.service.ts
// name.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class NameService {
private mName$ = new BehaviorSubject<string>(null);
name$(): Observable<string> {
return this.mName$;
}
constructor(private httpClient: HttpClient) { }
loadName() {
this.httpClient.get<string>('name URL')
.subscribe(
name => this.mName$.next(name)
);
}
}
To figure out what needs to be tested, let’s think about what could go wrong. There are 3 main things that are likely to go wrong. The first is that the component doesn’t even get created due to some syntactical or import error. The second is that the loadName function on the NameService doesn’t get called during initialisation. The third is that, once the NameService name$ function emits a name, it doesn’t get displayed in the component.
Reactive Component Tests
The following is the test code that checks the 3 things that are likely to go wrong described earlier.
name.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { NameService } from './name.service';
import { NameComponent } from './name.component';
describe('NameComponent', () => {
let nameServiceSpy: jasmine.SpyObj<NameService>;
let component: NameComponent;
let fixture: ComponentFixture<NameComponent>;
beforeEach(() => {
nameServiceSpy = jasmine.createSpyObj('NameService', ['loadName', 'name$']);
TestBed.configureTestingModule({
declarations: [ NameComponent ],
providers: [ { provide: NameService, useValue: nameServiceSpy } ]
});
fixture = TestBed.createComponent(NameComponent);
component = fixture.componentInstance;
});
it('should create', () => {
// Checking component creation
expect(component).toBeTruthy();
});
it('should call loadName on NameService', () => {
fixture.detectChanges();
// Checking loadName call
expect(nameServiceSpy.loadName).toHaveBeenCalledWith();
});
it('should display NameService name', () => {
// Setting name in service
nameServiceSpy.name$.and.returnValue(of('the name'));
fixture.detectChanges();
// Checking displayed name
expect(fixture.debugElement.nativeElement.innerText).toEqual('the name');
});
});
Test Setup
The setup common to all tests is on lines 8-21 which define the component under test and other supporting variables needed for he tests. Lines 13-21 define the standard TestBed initialisation and injection logic. The three test scenarios are defined on lines 24-27 (checking the component can be created), 29-34 (checking that loadName is called on NameService) and 36-44 (checking that the name emitted on name$ is displayed).
Component Setup
The test on lines 24-27 checks that the component can be setup. This is a simple test but is a very useful step for verifying that all the component plumbing is correct and that the test setup has actually worked. If this test fails it is likely because an injection is missing or because the TestBed was not setup correctly.
loadName Call
The test on lines 29-34 checks that the loadName function is called on the NameService. For the ngOnInit function to be triggered, change detection is run on line 30. Line 33 checks that loadName has been called on the NameService.
Name Display
The test on lines 36-44 checks that, when name$ on NameService emits a new name, that name is displayed in the component. Line 38 triggers emitting a new name on name$. Line 40 triggers change detection so that the name is displayed. Line 43 checks that the name is displayed in the component.
These tests are the base on which tests can be built if the logic in the component becomes more complicated. Examples of additional complications are having to pass arguments to the service loadName call, displaying data from multiple services or subjects and a more complicated template that includes conditional displaying of data.