【问题标题】:How to unit test a FormControl in Angular2如何在 Angular2 中对 FormControl 进行单元测试
【发布时间】:2017-01-21 17:19:20
【问题描述】:

我的测试方法如下:

/**
   * 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 进行单元测试的文档。我最接近的是这篇关于 Mocking Http requests 的文章,这是一个类似的概念,因为它们正在返回 Observables,但我认为它并不完全适用。

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

【问题讨论】:

    标签: unit-testing angular jasmine angular2-forms angular2-testing


    【解决方案1】:

    更新

    至于关于异步行为的答案的第一部分,我发现您可以使用 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 是异步的,因此在测试中的 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)是一个有用的功能,但问题是当我们在@Components 中使用templateUrl 时,它会进行XHR 调用,而XHR calls can't be made in a fakeAsync。在某些情况下,您可以模拟服务以使其同步,如this post 中所述,但在某些情况下,这不可行或太难了。在表格的情况下,这是不可行的。

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

    现在就表单的实际测试而言,我发现的最佳来源就是查看source code integration tests。您需要将标签更改为您使用的 Angular 版本,因为默认的 master 分支可能与您使用的版本不同。

    下面是几个例子。

    在测试输入时,您想要更改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 参数什么是由 change 事件发出的

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

    【讨论】:

      猜你喜欢
      • 2016-03-13
      • 2017-04-14
      • 1970-01-01
      • 2017-05-13
      • 1970-01-01
      • 1970-01-01
      • 2021-10-07
      • 2017-01-17
      • 2012-01-08
      相关资源
      最近更新 更多