0

I try to create a unittest for my angular component. The test case should do the following:

  1. Manipulate the input with "The"
  2. Check if the loading indicator is shown
  3. Return a mocked value from the service (which would normaly create a HttpRequest)
  4. Check if the loading indicator is hidden
  5. Check if the options of the response from the mocked service are shown
  6. [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);
        });
    }));

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for your answer and the guide, both was really helpfull.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.