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

查看:44
本文介绍了如何在 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,然后订阅它并检查返回值.

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

我似乎找不到任何关于对 FormControl 进行单元测试的文档.我最接近的是这篇关于 Mocking Http requests 的文章,这是一个类似的概念,因为它们返回 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).

为了解决这个问题,我们可以使用 fakeAsynctick 来强制测试等待所有当前的异步任务完成,让它在测试中看起来像如果它是同步的.

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 时,它使得XHR 调用,并且 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 版本,因为默认主分支可能与您使用的版本不同.

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天全站免登陆