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.