【问题标题】:Angular: Unittest with async input pipe + mock service with HttpClientAngular:带有异步输入管道的单元测试+带有HttpClient的模拟服务
【发布时间】:2021-10-18 14:40:06
【问题描述】:

我尝试为我的角度组件创建一个单元测试。测试用例应该做到以下几点:

  1. 使用“The”操作输入
  2. 检查是否显示加载指示器
  3. 从服务返回一个模拟值(通常会创建一个 HttpRequest)
  4. 检查加载指示器是否隐藏
  5. 检查是否显示来自模拟服务的响应选项
  6. [可选] 选择一个选项并检查 formControl 值

首先是我的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;
  }
}

HTML 文件:

<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>

这是我当前的单元测试。我无法为我的用例找到示例。我试图实施测试,就像他们在angular docs 中所做的那样。我还尝试了fixture.debugElement.query(By.css('input')) 来设置输入值并使用nativeElement,受到post 的启发,但都没有成功。我对角度单元测试不太熟悉。事实上,我可能还没有理解一些基本概念或原则。

    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);
        });
    }));

【问题讨论】:

    标签: javascript angular unit-testing


    【解决方案1】:

    单元测试的重点是它们应该很小。当然,您可以将 1 到 6 写为一个单元测试,但这会令人困惑。当我这样做时,想想单元测试,我明白了(一个动作,一个反应)。

    // 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);
    }));
    

    您不需要手动调用ngOnInit,因为component = 之后的第一个fixture.detectChanges() 会为您调用ngOnInit,而ngOnInit 只会为您填充一个可观察的流。

    This 似乎是 Angular 单元测试的一个很好的来源,尽管我还没有阅读所有内容。

    【讨论】:

    • 感谢您的回答和指导,两者都非常有帮助。
    猜你喜欢
    • 2021-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-27
    • 1970-01-01
    相关资源
    最近更新 更多