I try to create a unittest for my angular component. The test case should do the following:
- Manipulate the input with “The”
- Check if the loading indicator is shown
- Return a mocked value from the service (which would normaly create a HttpRequest)
- Check if the loading indicator is hidden
- Check if the options of the response from the mocked service are shown
- [optional] Select an option and check the formControl value
First of all my component.ts
:
@Component({ selector: 'app-band', templateUrl: './band.component.html', styleUrls: ['./band.component.scss'] }) export class BandComponent implements OnInit { loading?: boolean; formControl = new FormControl('', [Validators.minLength(3)]); filteredOptions: Observable<Band[]> | undefined; @Output() onBandChanged = new EventEmitter<Band>(); constructor(private bandService: BandService) { } ngOnInit(): void { this.filteredOptions = this.formControl.valueChanges .pipe( startWith(''), tap((value) => { if (value) this.loading = true; }), debounceTime(300), distinctUntilChanged(), switchMap(value => { if (!value || value.length < 3) { return of([]); } else { return this.bandService.searchFor(value).pipe(map(value => value.bands)) } }), tap(() => this.loading = false), ); } getBandName(band: Band): string { return band?.name; } }
The HTML file:
<mat-form-field class="input-full-width" appearance="outline"> <mat-label>Band</mat-label> <input matInput placeholder="e. G. Foo Fighters" type="text" [formControl]="formControl" [matAutocomplete]="auto"> <span matSuffix *ngIf="loading"> <mat-spinner diameter="24"></mat-spinner> </span> <mat-autocomplete #auto="matAutocomplete" [displayWith]="getBandName"> <mat-option *ngFor="let option of filteredOptions | async" [value]="option"> {{option.name}} </mat-option> </mat-autocomplete> <mat-error *ngIf="formControl.hasError('minlength')"> error message </mat-error> </mat-form-field>
Here is my current unittest. I was not able to find an example for my usecase. I tried to implement the test, like they did it in the angular docs. I also tried the fixture.debugElement.query(By.css('input'))
to set the input value and used the nativeElement
, inspired by this post, neither worked. I am not so familiar with angular unittests. In fact I might not have understood some base concepts or principles.
beforeEach(() => { bandService = jasmine.createSpyObj('BandService', ['searchFor']); searchForSpy = bandService.searchFor.and.returnValue(asyncData(testBands)); TestBed.configureTestingModule({ imports: [ BrowserAnimationsModule, FormsModule, ReactiveFormsModule, HttpClientTestingModule, MatAutocompleteModule, MatSnackBarModule, MatInputModule, MatProgressSpinnerModule ], providers: [{ provide: BandService, useValue: bandService }], declarations: [BandComponent], }).compileComponents(); fixture = TestBed.createComponent(BandComponent); component = fixture.componentInstance; loader = TestbedHarnessEnvironment.loader(fixture); fixture.detectChanges(); }); it('should search for bands starting with "The"', fakeAsync(() => { fixture.detectChanges(); component.ngOnInit(); tick(); const input = loader.getHarness(MatInputHarness); input.then((input) => { input.setValue('The'); fixture.detectChanges(); expect(component.loading).withContext('Showing loading indicator').toBeTrue(); tick(300); searchForSpy.and.returnValue(asyncData(testBands)); }).finally(() => { const matOptions = fixture.debugElement.queryAll(By.css('.mat-option')); expect(matOptions).toHaveSize(2); }); }));
Advertisement
Answer
The point of unit tests are that they should be small. Of course you can write 1 to 6 as one unit test but it will be confusing. Think of unit tests as I do this, I get that (one action, one reaction).
// 1 and 2 it('should show loading spinner if user types in input', fakeAsync(() => { // A good thing about using reactive forms is that you don't have to // use HTML and events, you can directly use setValue // Arrange and Act component.formControl.setValue('The'); fixture.detectChanges(); // expect expect(component.loading).toBeTrue(); const matSpinner = fixture.debugElement.query(By.css('mat-spinner')).nativeElement; expect(matSpinner).toBeTruthy(); })); // 3 and 4 it('should hide the loading spinner once data is retrieved', fakeAsync(() => { component.formControl.setValue('The'); // make 301 ms pass so it gets passed the debounceTime tick(301); // expectations expect(component.loading).toBeFalse(); const matSpinner = fixture.debugElement.query(By.css('mat-spinner')).nativeElement; expect(matSpinner).toBeFalsy(); })); // 5 and 6 (this one might be flaky, I am not sure how the HTML and classes // will be displayed it('should set the options', fakeAsync(() => { component.formControl.setValue('The'); // make 301 ms pass so it gets passed the debounceTime tick(301); // this may need some modifications const matOptions = fixture.debugElement.queryAll(By.css('.mat-option')); expect(matOptions).toHaveSize(2); }));
You don’t need to manually call ngOnInit
since the first fixture.detectChanges()
after component =
calls ngOnInit
for you and ngOnInit
only populates an observable stream for you.
This seems to be a good source for Angular Unit Testing although I haven’t read all of it.