簡體   English   中英

如何在Angular2中對FormControl進行單元測試

[英]How to unit test a FormControl in Angular2

我正在測試的方法如下:

/**
   * Update properties when the applicant changes the payment term value.
   * @return {Mixed} - Either an Array where the first index is a boolean indicating
   *    that selectedPaymentTerm was set, and the second index indicates whether
   *    displayProductValues was called. Or a plain boolean indicating that there was an 
   *    error.
   */
  onPaymentTermChange() {
    this.paymentTerm.valueChanges.subscribe(
      (value) => {
        this.selectedPaymentTerm = value;
        let returnValue = [];
        returnValue.push(true);
        if (this.paymentFrequencyAndRebate) { 
          returnValue.push(true);
          this.displayProductValues();
        } else {
          returnValue.push(false);
        }
        return returnValue;
      },
      (error) => {
        console.warn(error);
        return false;
      }
    )
  }

正如您所看到的,paymentTerm是一個返回Observable的表單控件,然后對其進行訂閱並檢查返回值。

我似乎無法找到有關單元測試FormControl的任何文檔。 我最接近的是這篇關於模擬Http請求的文章,這是一個類似的概念,因為它們正在返回Observables,但我不認為它完全適用。

作為參考,我使用Angular RC5,使用Karma運行測試,框架是Jasmine。

首先讓我們通過測試組件中的異步任務來解決一些常見問題。 當我們測試測試不受控制的異步代碼時,我們應該使用fakeAsync ,因為它允許我們調用tick() ,這使得操作在測試時看起來是同步的。 例如

class ExampleComponent implements OnInit {
  value;

  ngOnInit() {
    this._service.subscribe(value => {
      this.value = value;
    });
  }
}

it('..', () => {
  const fixture = TestBed.createComponent(ExampleComponent);
  fixture.detectChanges();
  expect(fixture.componentInstance.value).toEqual('some value');
});

當調用ngOnInit ,此測試將失敗,但Observable是異步的,因此在測試中(即expect )中的同步調用不會及時設置該值。

為了解決這個問題,我們可以使用fakeAsynctick來強制測試等待所有當前的異步任務完成,使測試看起來像是同步的。

import { fakeAsync, tick } from '@angular/core/testing';

it('..', fakeAsync(() => {
  const fixture = TestBed.createComponent(ExampleComponent);
  fixture.detectChanges();
  tick();
  expect(fixture.componentInstance.value).toEqual('some value');
}));

現在測試應該通過,因為Observable訂閱沒有意外延遲,在這種情況下我們甚至可以在tick tick(1000)傳遞毫秒延遲。

這個( fakeAsync )是一個很有用的功能,但問題是當我們在@Component使用templateUrl時,它會進行XHR調用,並且不能在fakeAsync進行XHR調用 在有些情況下,你可以嘲笑的服務,使之同步,如中提到的情況下這篇文章 ,但在某些情況下,這只是不可行或太困難。 在表格的情況下,這是不可行的。

出於這個原因,在處理表單時,我傾向於將模板放在template而不是外部的templateUrl ,如果它們非常大(只是在組件文件中沒有大字符串),則將表單分成更小的組件。 我能想到的唯一另一個選擇是在測試中使用setTimeout來讓異步操作通過。 這是一個偏好問題。 我剛決定在使用表單時使用內聯模板。 它打破了我的應用程序結構的一致性,但我不喜歡setTimeout解決方案。

現在,就表單的實際測試而言,我發現的最佳來源只是查看源代碼集成測試 您需要將標記更改為您正在使用的Angular版本,因為默認主分支可能與您使用的版本不同。

以下是一些例子。

在測試輸入時,您需要更改nativeElement上的輸入值,並使用dispatchEvent調度input事件。 例如

@Component({
  template: `
    <input type="text" [formControl]="control"/>
  `
})
class FormControlComponent {
  control: FormControl;
}

it('should update the control with new input', () => {
  const fixture = TestBed.createComponent(FormControlComponent);
  const control = new FormControl('old value');
  fixture.componentInstance.control = control;
  fixture.detectChanges();

  const input = fixture.debugElement.query(By.css('input'));
  expect(input.nativeElement.value).toEqual('old value');

  input.nativeElement.value = 'updated value';
  dispatchEvent(input.nativeElement, 'input');

  expect(control.value).toEqual('updated value');
});

這是從源集成測試中獲得的簡單測試。 下面是更多的測試示例,一個是從源代碼中獲取的,另外一個不是,只是為了顯示測試中沒有的其他方法。

對於您的特定情況,看起來您正在使用(ngModelChange) ,您在其中為onPaymentTermChange()分配調用。 如果是這種情況,那么您的實現沒有多大意義。 (ngModelChange)在值發生變化時已經吐出一些東西,但每次模型更改時都會訂閱。 你應該做的是接受更改事件發出的$event參數

(ngModelChange)="onPaymentTermChange($event)"

每次更改時,您都會傳遞新值。 所以只需在您的方法中使用該值,而不是訂閱。 $event將是新值。

如果想使用valueChangeFormControl ,則應該開始聽它在ngOnInit ,所以你只能預訂一次。 你會在下面看到一個例子。 我個人不會去這條路。 我會按照您的方式進行,但不是訂閱更改,只需接受更改中的事件值(如前所述)。

這是一些完整的測試

import {
  Component, Directive, EventEmitter,
  Input, Output, forwardRef, OnInit, OnDestroy
} from '@angular/core';
import { Subscription } from 'rxjs/Subscription';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser/src/dom/debug/by';
import { getDOM } from '@angular/platform-browser/src/dom/dom_adapter';
import { dispatchEvent } from '@angular/platform-browser/testing/browser_util';
import { FormControl, ReactiveFormsModule } from '@angular/forms';

class ConsoleSpy {
  log = jasmine.createSpy('log');
}

describe('reactive forms: FormControl', () => {
  let consoleSpy;
  let originalConsole;

  beforeEach(() => {
    consoleSpy = new ConsoleSpy();
    originalConsole = window.console;
    (<any>window).console = consoleSpy;

    TestBed.configureTestingModule({
      imports: [ ReactiveFormsModule ],
      declarations: [
        FormControlComponent,
        FormControlNgModelTwoWay,
        FormControlNgModelOnChange,
        FormControlValueChanges
      ]
    });
  });

  afterEach(() => {
    (<any>window).console = originalConsole;
  });

  it('should update the control with new input', () => {
    const fixture = TestBed.createComponent(FormControlComponent);
    const control = new FormControl('old value');
    fixture.componentInstance.control = control;
    fixture.detectChanges();

    const input = fixture.debugElement.query(By.css('input'));
    expect(input.nativeElement.value).toEqual('old value');

    input.nativeElement.value = 'updated value';
    dispatchEvent(input.nativeElement, 'input');

    expect(control.value).toEqual('updated value');
  });

  it('it should update with ngModel two-way', fakeAsync(() => {
    const fixture = TestBed.createComponent(FormControlNgModelTwoWay);
    const control = new FormControl('');
    fixture.componentInstance.control = control;
    fixture.componentInstance.login = 'old value';
    fixture.detectChanges();
    tick();

    const input = fixture.debugElement.query(By.css('input')).nativeElement;
    expect(input.value).toEqual('old value');

    input.value = 'updated value';
    dispatchEvent(input, 'input');
    tick();

    expect(fixture.componentInstance.login).toEqual('updated value');
  }));

  it('it should update with ngModel on-change', fakeAsync(() => {
    const fixture = TestBed.createComponent(FormControlNgModelOnChange);
    const control = new FormControl('');
    fixture.componentInstance.control = control;
    fixture.componentInstance.login = 'old value';
    fixture.detectChanges();
    tick();

    const input = fixture.debugElement.query(By.css('input')).nativeElement;
    expect(input.value).toEqual('old value');

    input.value = 'updated value';
    dispatchEvent(input, 'input');
    tick();

    expect(fixture.componentInstance.login).toEqual('updated value');
    expect(consoleSpy.log).toHaveBeenCalledWith('updated value');
  }));

  it('it should update with valueChanges', fakeAsync(() => {
    const fixture = TestBed.createComponent(FormControlValueChanges);
    fixture.detectChanges();
    tick();

    const input = fixture.debugElement.query(By.css('input')).nativeElement;

    input.value = 'updated value';
    dispatchEvent(input, 'input');
    tick();

    expect(fixture.componentInstance.control.value).toEqual('updated value');
    expect(consoleSpy.log).toHaveBeenCalledWith('updated value');
  }));
});

@Component({
  template: `
    <input type="text" [formControl]="control"/>
  `
})
class FormControlComponent {
  control: FormControl;
}

@Component({
  selector: 'form-control-ng-model',
  template: `
    <input type="text" [formControl]="control" [(ngModel)]="login">
  `
})
class FormControlNgModelTwoWay {
  control: FormControl;
  login: string;
}

@Component({
  template: `
    <input type="text"
           [formControl]="control" 
           [ngModel]="login" 
           (ngModelChange)="onModelChange($event)">
  `
})
class FormControlNgModelOnChange {
  control: FormControl;
  login: string;

  onModelChange(event) {
    this.login = event;
    this._doOtherStuff(event);
  }

  private _doOtherStuff(value) {
    console.log(value);
  }
}

@Component({
  template: `
    <input type="text" [formControl]="control">
  `
})
class FormControlValueChanges implements OnDestroy {
  control: FormControl;
  sub: Subscription;

  constructor() {
    this.control = new FormControl('');
    this.sub = this.control.valueChanges.subscribe(value => {
      this._doOtherStuff(value);
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe();
  }

  private _doOtherStuff(value) {
    console.log(value);
  }
}

UPDATE

至於關於異步行為的這個答案的第一部分,我發現你可以使用fixture.whenStable()來等待異步任務。 因此無需僅使用內聯模板

it('', async(() => {
  fixture.whenStable().then(() => {
    // your expectations.
  })
})

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM