web-dev-qa-db-ja.com

Angular2でFormControlを単体テストする方法

テスト対象のメソッドは次のとおりです。

/**
   * 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を返すフォームコントロールです。Observableはサブスクライブされ、戻り値がチェックされます。

FormControlの単体テストに関するドキュメントが見つからないようです。最も近いのは、Httpリクエストのモックに関するこの記事です。これは、Observablesを返すのと同様の概念ですが、完全に当てはまるとは思いません。

参考のために、私はAngular RC5を使用しています。Karmaでテストを実行し、フレームワークはJasmineです。

17
chap

最初に、コンポーネントの非同期タスクのテストに関する一般的な問題に取り組みましょう。テストが制御していない非同期コードをテストする場合は、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は非同期であるため、値はsynchronus呼び出しの時間内に設定されませんテスト(つまりexpect)。

これを回避するために、fakeAsyncおよびtickを使用して、現在のすべての非同期タスクが完了するまでテストを待機させ、テストが同期されているように見せることができます。

_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(1000)でミリ秒の遅延を渡すことさえできます。

これ(fakeAsync)は便利な機能ですが、問題は_@Component_ sでtemplateUrlを使用すると、XHR呼び出しが行われ、 XHR呼び出しができることです'fakeAsync で作成されません。 this post で述べたように、サービスをモックして同期化することができる状況がありますが、場合によっては実行不可能または難しすぎます。フォームの場合、実行不可能です。

このため、フォームを操作するとき、テンプレートを外側のtemplateの代わりにtemplateUrlに配置し、本当に大きい場合はフォームを小さなコンポーネントに分割する傾向があります(単にコンポーネントファイル内の巨大な文字列)。私が考えることができる他の唯一のオプションは、テスト内でsetTimeoutを使用して、非同期操作を通過させることです。それは好みの問題です。フォームを操作するときは、インラインテンプレートを使用することにしました。それは私のアプリ構造の一貫性を壊しますが、setTimeoutソリューションは好きではありません。

フォームの実際のテストに関する限り、私が見つけた最良のソースは、 ソースコード統合テスト を調べることだけでした。タグをAngularのバージョンに変更することをお勧めします。デフォルトのマスターブランチは使用しているバージョンと異なる場合があるためです。

以下にいくつかの例を示します。

入力をテストするとき、nativeElementの入力値を変更し、inputを使用してdispatchEventイベントをディスパッチします。例えば

_@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');
});
_

これは、ソース統合テストから取得した簡単なテストです。以下に、テストにない他の方法を示すために、より多くのテスト例、ソースから取得したもの、およびそうでないカップルを示します。

特定のケースでは、onPaymentTermChange()への呼び出しを割り当てる_(ngModelChange)_を使用しているように見えます。この場合、実装はあまり意味がありません。 _(ngModelChange)_は、値が変更されるとすでに何かを吐き出しますが、モデルが変更されるたびにサブスクライブしています。あなたがすべきことは、変更イベントによって発行される_$event_パラメータを受け入れることです

_(ngModelChange)="onPaymentTermChange($event)"
_

変更するたびに新しい値が渡されます。したがって、サブスクライブする代わりに、メソッドでその値を使用します。 _$event_が新しい値になります。

dovalueChangeFormControlを使用する場合は、代わりに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);
  }
}
_

更新

非同期動作に関するこの回答の最初の部分に関しては、非同期タスクを待機するfixture.whenStable()を使用できることがわかりました。したがって、インラインテンプレートのみを使用する必要はありません

_it('', async(() => {
  fixture.whenStable().then(() => {
    // your expectations.
  })
})
_
33
Paul Samsotha