如何在Angular2中对FormControl进行单元测试 [英] How to unit test a FormControl in Angular2

查看:114
本文介绍了如何在Angular2中对FormControl进行单元测试的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在测试的方法如下:

My method under test is the following:

/**
   * 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然后被订阅并检查了返回值。

As you can see paymentTerm is a form control that returns an Observable, which is then subscribed and the return value is checked.

我似乎找不到有关单元测试FormControl的任何文档。与我最近的文章是有关模拟Http请求的文章,这是一个类似的概念,因为它们正在返回Observables,但我认为它并不完全适用。

I can't seem to find any documentation on unit testing a FormControl. The closest I have come is this article about Mocking Http requests, which is a similar concept as they are returning Observables but I don't think it applies fully.

作为参考,我使用Angular RC5,使用Karma运行测试,框架为Jasmine。

For reference I am using Angular RC5, running tests with Karma and the framework is Jasmine.

推荐答案

UPDATE



至此答案的第一部分关于异步行为,我发现您可以使用 fixture.whenStable()来等待异步任务。因此无需只使用内联模板

UPDATE

As far as the first part of this answer about the asynchronous behavior, I've found out that you can use fixture.whenStable() which will wait for asynchronous tasks. So no need to only use inline templates

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






首先让我们了解一些测试组件中异步任务的常规问题,当我们测试异步代码时,该测试不在控制,我们应该使用 fakeAsync ,因为它将允许我们调用 tick(),从而使操作出现例如,


First let we get to some general problems with testing asynchronous tasks in components. When we test asynchronous code that the test is not in control of, we should use fakeAsync, as it will allow us to call tick(), which makes the actions appears synchronous when testing. For example

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

This test is going to fail as the ngOnInit is called, but the Observable is asynchronous, so the value doesn't get set in time for the synchronus calls in the test (i.e. the expect).

要解决此问题,我们可以使用 fakeAsync tick 强制测试等待所有当前异步任务完成,从而使其在测试中看起来像是在同步。

To get around this, we can use the fakeAsync and tick to force the test to wait for all current asynchronous tasks to finish, making it appear to the test as if it where synchronous.

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)中传递毫秒级延迟。

Now the test should pass, given there is no unexpected delay in the Observable subscription, in which case we can even pass a millisecond delay in the tick call tick(1000).

此( fakeAsync )是一个有用的功能,但是问题在于,当我们在 @Component templateUrl 时c $ c> s,它会进行XHR调用,并且不能在 fakeAsync 。如这篇文章所述,在某些情况下您可以模拟该服务以使其同步。不可行或太困难。

This (fakeAsync) is a useful feature, but the problem is that when we use templateUrl in our @Components, it makes an XHR call, and XHR calls can't be made in a fakeAsync. There are situations where you can mock the service to make it synchronous, as mentioned in this post, but in some cases it's just not feasible or just too difficult. In the case of forms, it's just not feasible.

因此,在处理表单时,我倾向于将模板放在 template 而不是外部的 templateUrl ,如果它们很大,则将表单分成较小的组件(只是在组件文件中没有很大的字符串)。我唯一想到的另一种选择是在测试中使用 setTimeout ,以使异步操作通过。这是个人喜好问题。我刚决定在处理表单时使用内联模板。它破坏了我的应用程序结构的一致性,但是我不喜欢 setTimeout 解决方案。

For this reason, when working with forms, I tend to put the templates in template instead of an outside templateUrl and break the form into smaller components if they are really big (just to not have a huge string in the component file). The only other option I can think of is to use a setTimeout inside the test, to let the asynchronous operation pass. It's a matter of preference. I just decided to go with the inline templates when workings with forms. It breaks the consistency of my app structure, but I don't like the setTimeout solution.

现在作为对表单的实际测试,我发现的最佳来源只是查看源代码集成测试。您需要将标签更改为您使用的Angular版本,因为默认的master分支可能与您使用的版本不同。

Now as far as the actual testing for forms, the best source I found was just to look at the source code integration tests. You'll want to change the tag to the version of Angular you are using, as the default master branch may be different from the version you are using.

下面是一个几个例子。

在测试输入时,要更改 nativeElement 上的输入值,然后调度使用 dispatchEvent input 事件。例如

When testing inputs what you want to is change the input value on the nativeElement, and the dispatch an input event using dispatchEvent. For example

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

这是从源集成测试中提取的一个简单测试。下面有更多测试示例,另外一个是从源中抽取的,还有两个没有的示例,只是为了显示其他不在测试中的方式。

This is a simple test taken from the source integration test. Below there are more test examples, one more taken from the source, and a couple that are not, just to show other ways that are not in the tests.

在特定情况下,您似乎正在使用(ngModelChange),在其中将其分配给 onPaymentTermChange()。在这种情况下,您的实施就没有多大意义了。 (ngModelChange)已经准备在值更改时吐出一些东西,但是每次模型更改时您都在订阅。您应该做的是接受 $ event 参数,更改事件发出的信息

For your particular case, it looks like you are using the (ngModelChange), where you are assigning it the call to onPaymentTermChange(). If this is the case, your implementation doesn't make much sense. (ngModelChange) is already going to spit out something when the value changes, but you are subscribing each time the model changes. What you should be doing is accepting the $event parameter what is emitted by the change event

(ngModelChange)="onPaymentTermChange($event)"

您每次更改都会获得新值。因此,只需在您的方法中使用该值即可,而不是订阅。 $ event 将是新值。

You will get passed the new value every time it changes. So just use that value in your method, instead of subscribing. The $event will be the new value.

如果您愿意使用 FormControl 上的 valueChange ,您应该改为在 ngOnInit ,因此您只订阅一次。您将在下面看到一个示例。就我个人而言,我不会走这条路线。我会按照您的方式去做,但是除了订阅更改之外,只接受更改中的事件值(如前所述)即可。

If you do want to use the valueChange on the FormControl, you should instead start listening to it in ngOnInit, so you are only subscribing once. You'll see an example below. Personally I wouldn't go this route. I would just go with the way you are doing, but instead of subscribing on the change, just accept the event value from the change (as previously described).

这里是一些完整的测试

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

这篇关于如何在Angular2中对FormControl进行单元测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆