如何测试 Angular 的 ngModel?

发表于:2017-8-21 14:14

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:未知    来源:51Testing软件测试网采编

   Two-way Binding 的 ngModel 非常方便,但因为这是 syntax sugar,该如何测试呢?
  Version
  Angalar 4.3
  Requirement
  画面有一个下拉选单。
  下拉选单内有 AWS 、 Azure 与 Aliyun 三个值。
  当选择 AWS 时,在下方显示 0 。
  当选择 Azure 时,在下方显示 1 。
  当选择 Aliyun 时,在下方显示 2 。
  Acceptance Test (红灯)
  测试案例
  下拉选单应该有 3 个选项
  选择 AWS ,下方应该出现 0
  选择 Azure 时,下方应该出现 1
  选择 Aliyun 时,下方应该出现 2
  e2e/app.e2e-spec.ts
  import { NG43ATDDSelectPage } from './app.po';
  describe('NG43ATDDSelect', () => {
    let page: NG43ATDDSelectPage;
    beforeEach(() => {
      page = new NG43ATDDSelectPage();
    });
    it('should have 3 options in select', () => {
      page.navigateTo();
      expect(page.getSelectCount()).toBe(3);
    });
    it(`should show '0' when selecting 'AWS'`, () => {
      page.navigateTo();
      page.select('AWS');
      expect(page.getSelectedId()).toBe('0');
    });
    it(`should show '1' when selecting 'Azure'`, () => {
      page.navigateTo();
      page.select('Azure');
      expect(page.getSelectedId()).toBe('1');
    });
    it(`should show '2' when selecting 'Aliyun'`, () => {
      page.navigateTo();
      page.select('Aliyun');
      expect(page.getSelectedId()).toBe('2');
    });
  });
  10 行
  it(`should have 3 options in select`, () => {
    page.navigateTo();
    expect(page.getSelectCount()).toBe(3);
  });
  测试下拉选单的资料是否为 AWS 、 Azure 与 Aliyun ,实务上这些资料会从后端 API 来,可测试 API 是否正常接上,可以测试 API 的 SQL 或 ORM 是否正确。
  要测试资料完全相同比较困难时,最少可以测试资料的笔数是否正确。
  15 行
  it(`should show '0' when selecting 'AWS' `, () => {
    page.navigateTo();
    page.select('AWS');
    expect(page.getSelectedId()).toBe('0');
  });
  当选择 AWS 时,下方应该显示 0 。
  剩下 Azure 与 Aliyun 的写法类似。
  1、编辑 e2e/app.e2e-spec.ts
  2、加入验收测试
  e2e/app.po.ts
   import { browser, by, element } from 'protractor';
  export class NG43ATDDSelectPage {
    navigateTo() {
      return browser.get('/');
    }
    getSelectCount(): any {
      return element(by.id('TDDSelect')).all(by.tagName('option')).count();
    }
    select(text: string) {
      element(by.id('TDDSelect')).all(by.cssContainingText('option', text)).click();
    }
    getSelectedId(): any {
      return element(by.css('p')).getText();
    }
  }
  第 8 行
  getSelectCount(): any {
    return element(by.id('TDDSelect')).all(by.tagName('option')).count();
  }
  先由 element(by.id('TDDSelect')) 先抓到 <select> ,再由 all() 取得 <select> 下所有 element,再将 by.tagName('option') 传入 all() ,找出 tag 为 <option> 的 element 加以 count() 。
  12 行
  select(text: string) {
    element(by.id('TDDSelect')).all(by.cssContainingText('option', text)).click();
  }
  先由 element(by.id('TDDSelect')) 先抓到 <select> ,再由 all() 取得 <select> 下所有 element,再将 by.cssContainingText('option', text) 取得正确 option 加以 click() 。
  使用 Protractor 在下拉选单选择指定字符串的写法较为 tricky,需动用 by.cssContainingText() 。
  16 行
  getSelectedId(): any {
    return element(by.css('p')).getText();
  }
  下拉选单所选的值,预期只会用 <p> 包起来而已,因此 by.css('p') 即可。
  1.编辑 e2e/app.po.ts
  2.加入 page object
  因为我们还没实作此功能,得到预期的验收测试 红灯 。
  Integration Test (红灯)
  测试案例
  ngModel 应该使用 selectedId field
  <p> 内应该使用 selectedId field
  triggerEventHandler()
  src/app/app.component.spec.ts
  import { ComponentFixture, TestBed } from '@angular/core/testing';
  import { AppComponent } from './app.component';
  import { DebugElement } from '@angular/core';
  import { By } from '@angular/platform-browser';
  import { FormsModule } from '@angular/forms';
  describe('AppComponent', () => {
    let fixture: ComponentFixture<AppComponent>;
    let component: AppComponent;
    let debugElement: DebugElement;
    let htmlElement: HTMLElement;
    let target: AppComponent;
    beforeEach(() => {
      TestBed.configureTestingModule({
        declarations: [
          AppComponent
        ],
        imports: [
          FormsModule
        ],
      });
      fixture = TestBed.createComponent(AppComponent);
      component = fixture.componentInstance;
      debugElement = fixture.debugElement;
      target = new AppComponent();
      fixture.detectChanges();
    });
    describe(`ATDDSelect`, () => {
      describe(`Integration Test`, () => {
        it(`should have 'selectedId' field on 'ngModel' directive`, () => {
          debugElement.query(By.css('#TDDSelect')).triggerEventHandler('change', {target: {value: '2'}});
          expect(component.selectedId).toBe('2');
        });
        it(`should use 'selectedId' field`, () => {
          component.selectedId = '1';
          fixture.detectChanges();
          htmlElement = debugElement.query(By.css('p')).nativeElement;
          expect(htmlElement.textContent).toBe('1');
        });
      });
    });
  });
  34 行
  it(`should have 'selectedId' field on 'ngModel' directive`, () => {
    debugElement.query(By.css('#TDDSelect')).triggerEventHandler('change', {target: {value: '2'}});
    expect(component.selectedId).toBe('2');
  });
  测试案例 : ngModel 应该使用 selectedId field
  ngModel 就是要将 HTML element 直接绑定到 field,因此最重要的就是测试有没有绑定到正确的 field。
  Two-way binding 的重点在于当 HTML 改变时,class 的 field 会自动跟着改变,因此我们测试的手法就是改变 HTML,并测试 field 是否跟着 HTML 变化。
  debugElement.query(By.css('#TDDSelect')).triggerEventHandler('change', {target: {value: '2'}});
  在整合测试时,我们惯用 triggerEventHandler() 去触发 event,由于 DOM 的 change event 会触发 ngModelChange event,因此我们使用 triggerEventHandler() 去触发 change event。
  我们对 <select> 做任何选择的 value 值,会由 change() 的第 2 个参数的 $event.target.value 代入。
  因此我们要准备 {target: {value: '2'}} 这个 stub,并测试 field 值否为此 stub,若相等,怎表示 ngModel 有绑定到正确的 field。
  expect(component.selectedId).toBe('2');
  期望 component.selectedId 为 2 这个 stub。
  40 行
  it(`should use 'selectedId' field`, () => {
    component.selectedId = '1';
    fixture.detectChanges();
    htmlElement = debugElement.query(By.css('p')).nativeElement;
    expect(htmlElement.textContent).toBe('1');
  });
  测试案例 : <p> 应该使用 selectedId field
  因为目前是整合测试,而 AppComponent 的 selectedId 根本还没实现,理论上也应该使用 spyOn() ,但可惜 Jasmine 的 spyOn() 并没有支援 field,只能使用最基本的方式: 建立假物件,并测试假物件 的方式。
  因为还没实作,整合测试是预期的 红灯 。
  dispatchEvent()
  src/app/app.component.spec.ts
  import { ComponentFixture, TestBed } from '@angular/core/testing';
  import { AppComponent } from './app.component';
  import { DebugElement } from '@angular/core';
  import { By } from '@angular/platform-browser';
  import { FormsModule } from '@angular/forms';
  describe('AppComponent', () => {
    let fixture: ComponentFixture<AppComponent>;
    let component: AppComponent;
    let debugElement: DebugElement;
    let htmlElement: HTMLElement;
    let target: AppComponent;
    beforeEach(() => {
      TestBed.configureTestingModule({
        declarations: [
          AppComponent
        ],
        imports: [
          FormsModule
        ],
      });
      fixture = TestBed.createComponent(AppComponent);
      component = fixture.componentInstance;
      debugElement = fixture.debugElement;
      target = new AppComponent();
      fixture.detectChanges();
    });
    describe(`ATDDSelect`, () => {
      describe(`Integration Test`, () => {
        it(`should have 'selectedId' field on 'ngModel' directive`, () => {
          htmlElement = debugElement.query(By.css('#TDDSelect')).nativeElement;
          (<HTMLSelectElement>htmlElement).value = '2';
          htmlElement.dispatchEvent(new Event('change'));
          expect(component.selectedId).toBe('2');
        });
        it(`should use 'selectedId' field`, () => {
          component.selectedId = '1';
          fixture.detectChanges();
          htmlElement = debugElement.query(By.css('p')).nativeElement;
          expect(htmlElement.textContent).toBe('1');
        });
      });
    });
  });
  34 行
  it(`should have 'selectedId' field on 'ngModel' directive`, () => {
    htmlElement = debugElement.query(By.css('#TDDSelect')).nativeElement;
    (<HTMLSelectElement>htmlElement).value = '2';
    htmlElement.dispatchEvent(new Event('change'));
    expect(component.selectedId).toBe('2');
  });
  使用 triggerEventHandler() 方式虽然可行,但我们发现其第 2 个参数的 stub : {target: {value: '2'}} 有点小复杂,且必须很了解 $event 物件的结构。
  (<HTMLSelectElement>htmlElement).value = '2';
  当 <select> 的选择改变时,事实上就是改变其 value 值。
  htmlElement.dispatchEvent(new Event('change'));
  使用 dispatchEvent() 触发 event,传入 Event 物件,并以 evetn 名称传入 Event 。
  triggerEventHandler() 与 dispatchEvent() 都是触发 event,只是 triggerEventHandler() 是从 DOM 的方式去触发 event,而 dispatchEvent() 是从 Angular 的方式触发 event,两种方式都可以,实务上推荐使用 dispatchEvent() ,就不用了解 DOM 的 $event 物件,也比较物件导向。
  因为还没实作,整合测试是预期的 红灯 。
  Unit Test (红灯)
  因为 ngModel 不须要 class 实作任何 method,因此也不需要单元测试
  Unit Test (绿灯)
  因为 ngModel 没有单元测试,因此也不需要通过单元测试。
  Integration Test (绿灯)
  测试案例 : ngModel 应该使用 selectedId field
  src/app/app.component.html
  <select id="TDDSelect" [(ngModel)]="selectedId">
    <option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
  </select>
  <p></p>
  在 <select> 加上 [(ngModel)]="selectedId" 。
  在 HTML 实作 ngModel 绑定到 selectedId field 之后,使用 triggerEventHandler() 写法的整合测试就 绿灯 了。
  使用 dispatchEvent() 写法的整合测试也 绿灯 了。
  src/app/app.component.html
  测试案例 : <p> 内应该使用 selectedId field
  <select id="TDDSelect" [(ngModel)]="selectedId">
    <option *ngFor="let cloud of clouds" [value]="cloud.id">{{ cloud.name }}</option>
  </select>
  <p>{{ selectedId }}</p>
  在 <p> 内加上 selectedId 。
  在 HTML 实作 <p> 内的 selectedId 绑定之后,使用 triggerEventHandler() 写法的整合测试就 绿灯 了。
  使用 dispatchEvent() 写法的整合测试也 绿灯 了。
  Acceptance Test (绿灯)
  测试案例
  1.下拉选单应该有 3 个选项
  2.选择 AWS 时,下方应该出现 0
  3.选择 Azure 时,下方应该出现 1
  4.选择 Aliyun 时,下方应该出现 2
整合测试 绿灯 后,最后再跑一次验收测试确认为 绿灯 。
  Refactoring
  因为 class 没有逻辑,所以不需要重构。
  Conclusion
  ngModel 只有验收测试与整合测试,没有单元测试,因为 class 没有逻辑。
  ngModel 整合测试的关键在于测试 ngModel 有没有绑定到正确的 field,因此建立 stub,并触发 DOM 的 change event ,测试 field 值是否为 stub,则完成 ngModel 的整合测试。
  在整合测试触发 event 时,可以使用 triggerEventHandler() 或 dispatchEvent() ,实务上建议使用 dispatchEvent() ,较物件导向。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号