如何在Angular2中对FormControl进行单元测试

25

我要测试的方法如下:

/**
   * 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。

1个回答

51

更新

关于异步行为的第一部分,我发现你可以使用 fixture.whenStable() 来等待异步任务。因此不需要仅使用内联模板。

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

首先,让我们来谈谈关于在组件中测试异步任务的一些常见问题。当我们测试异步代码时,由于测试不受控制,因此我们应该使用 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请求,而XHR请求不能在fakeAsync中发出。 有些情况下,您可以模拟服务使其同步,如此帖子中所述,但在某些情况下,这就不可行或者太困难了。 在表单的情况下,根本不可行。

因此,当处理表单时,我倾向于将模板放在template中,而不是外部的templateUrl中,并且如果表单实在很大,我会将其拆分成较小的组件(仅为了不在组件文件中具有巨大的字符串)。 我能想到的唯一另一种选择是在测试中使用setTimeout,以让异步操作通过。 这是个人偏好。 我决定在处理表单时使用内联模板。 这打破了我的应用程序结构的一致性,但我不喜欢setTimeout的解决方案。

现在就实际测试表单而言,我找到最好的来源只是看源代码集成测试。您需要将标签更改为您正在使用的Angular版本,因为默认的主分支可能与您正在使用的版本不同。

以下是一些示例。

在测试输入时,您要做的就是更改nativeElement上的输入值,并使用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');
});

这是一个简单的测试,取自源集成测试。下面有更多的测试例子,其中一个来自源代码,还有一些其他的方式展示了不在测试中的方法。

针对你的特定情况,看起来你正在使用(ngModelChange),将其分配为onPaymentTermChange()的调用。如果是这种情况,那么你的实现没有太多意义。(ngModelChange)已经会在值改变时输出某些内容,但你却每次订阅模型发生变化。你应该做的是接受由更改事件发出的$event参数。

(ngModelChange)="onPaymentTermChange($event)"
每次值改变时,都会传入新的值。因此,在您的方法中使用该值,而不是订阅它。$event将是新值。
如果您确实希望在FormControl上使用valueChange,则应在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);
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接