第 29 型 - 单元测试 (Unit Testing)

在应用程序开发中,常会利用人工手动去测试系统的正确,不过当遇到较复杂的系统时,手动测试相对耗时且容易遗漏。除此之外,也可以撰写单元测试 (Unit Testing) 程序并利用测试框架运行,来验证系统的正确性;此通常针对的是方法或是类别,透过封装隔离来缩小测试的范围,以在持续开发与维护中确保程序品质。这一篇将了解在 Angular 应用程序如何针对元件进行单元测试 (Unit Test)。

Jasmine

Angular 官方建议使用 Jasmine 来撰写测试程序,将测试程序撰写在 *.spec.ts 档案中,透过下列几个方法来定义各种的测试案例。

describe('测试分组描述', () => {
  beforeAll(() => { });

  afterAll(() => { });

  beforeEach(() => { });

  it('测试案例描述', () => { });

  afterEach(() => { });
});

每一个测试案例皆会写在 it() 方法之中,且每一个案例皆不互相影响;当测试案例愈来愈多时,可以利用 describe() 方法来分组,此方法可以有一至多个 describe()it() 方法。在执行 describe 下的所有测试案例之前,会触发一次 beforeAll() 方法,此方法用於初始化所有案例所需的物件;当所有测试案例完成後,则会触发一次 afterAll() 方法来注销全域物件。而在每一个案例在执行的前後则分别会触发 beforeEach()afterEach() 方法。另外,可以在 describe()it() 前加上 f,来只执行特定的测试案例;或是加上 x 来排除不执行该测试案例。

Angular 测试环境

在执行测试时,Angular 会透过 TestBed 动态建立用来模拟 @NgModule 的测试模组,因此会在此针对所测试的元件对象汇入所需要的模组或元件。

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [TaskComponent]
    })
    .compileComponents();
  }));

利用单元测试验证待办事项主旨的显示正确性

大致了解 Jasmine 的方法後,首先在 task.component.spec.ts 中新增验证主旨显示的测试案例。

describe('TaskComponent', () => {
  it('当指定主旨为 "页面需要显示待办事项主旨", 页面应显示为 "页面需要显示待办事项..."', () => {
  });
});

在撰写单元测试程序时会采用 3A 原则来加强测试程序的可读性;此原则包含了:

  • Arrange - 初始化所需要的资讯,或预期的结果
  • Act - 实际执行所要测试的对象
  • Assert - 验证执行的结果是否符合预期
describe('TaskComponent', () => {
  it('当指定主旨为 "页面需要显示待办事项主旨", 页面应显示为 "页面需要显示待办事项..."', () => {
    // arrange
    const expected = '页面需要显示待办事项...';

    // act

    // assert
  });
});

另外,在对元件进行测试时,Angular 会利用 TestBed 建立元件的实体,并提供 ComponentFixture 物件来对元件与其 DOM 进行互动。因此此测试案例会针对 fixture 物件的元件实体变数 component 设定主旨属性,并呼叫 detectChanges() 方法让 TestBed 执行资料系结。

describe('TaskComponent', () => {
  it('当指定主旨为 "页面需要显示待办事项主旨", 页面应显示为 "页面需要显示待办事项..."', () => {
    // arrange
    const expected = '页面需要显示待办事项...';

	// act
    component.subject = '页面需要显示待办事项主旨';
    fixture.detectChanges();

    // assert
  });
});

最後需要验证页面显示的主旨是否符合预期,可以查询 ComponentFixture 内的 DebugElement 树来取得到 DebugElement 节点,进一步使用 nativeElement 来取得执行平台的原生物件,而指定的方式可以利用 css 选择器或是直接指定元件类型。然後利用 Jasmine 提供的方法进行验证後,就可以执行 ng test 来运行测试程序了。

describe('TaskComponent', () => {
  it('当指定主旨为 "页面需要显示待办事项主旨", 页面应显示为 "页面需要显示待办事项..."', () => {
    // arrange
    const expected = '页面需要显示待办事项...';

	// act
    component.subject = '页面需要显示待办事项主旨';
    fixture.detectChanges();

	// assert
    const debugElement = fixture.debugElement.query(By.css('div.content span'));
    expect(debugElement.nativeElement.textContent).toContain(expected);
  });
});

Testing

验证待办事项清单显示个数是否正确

当元件有注入服务时,所撰写单元测试一般不会直接使用实际的服务,而是建立一个 Spy 服务物件来模拟,如此才可以控制元件的输入与输出的状态。不过,因为在 TaskList 元件中注入了 Router 服务元件,并且有使用到 Tsak 元件,所以在 task-list.component.spec.ts 中需要先汇入所需要的模组与元件。

describe('TaskListComponent', () => {
  let component: TaskListComponent;
  let fixture: ComponentFixture<TaskListComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [RouterTestingModule, HttpClientModule, FormsModule],
      declarations: [TaskListComponent, TaskComponent, TaskStateColorDirective],
    }).compileComponents();
  }));
});

接着,利用 Jasmine 提供的 createSpyObj 来建立一个待办事项服务的 Spy 物件,且定义此服务元件 getData() 所回传的值,并注入在 TestBed 内以取代实际的待办事项服务。

describe('TaskListComponent', () => {
  let component: TaskListComponent;
  let fixture: ComponentFixture<TaskListComponent>;

  let taskService: jasmine.SpyObj<TaskRemoteService>;

  beforeEach(async(() => {
    taskService = jasmine.createSpyObj(['getData']);
    taskService.getData.and.returnValue(
      of([
        {
          id: 1,
          subject: '页面需要显示待办事项主旨',
          state: 0,
          level: 'XS',
          tags: ['FEATURE', 'ISSUE', 'enhancement', 'discussion'],
        },
      ])
    );

    TestBed.configureTestingModule({
      imports: [RouterTestingModule, HttpClientModule, FormsModule],
      declarations: [TaskListComponent, TaskComponent, TaskStateColorDirective],
      providers: [{ provide: TaskRemoteService, useValue: taskService }],
    }).compileComponents();
  }));
});

最後建立一个测试案例,直接利用 TaskComponent 来查询页面上所有 DebugElement 节点,并验证其个数是否符合服务所设定的个数。

  fit('应显示一笔待办事项', () => {
    const debugElements = fixture.debugElement.queryAll(By.directive(TaskComponent));
    expect(debugElements.length).toBe(1);
  });

结论

利用单元测试可以将测试案例记录下,在持续的开发与维护中重覆的执行,减少人工测试的时间成本,更进一步的搭配 CI 来避免把有问题的程序更新至正式环境中。


<<:  RxJS 错误处理 Operators (1) - catchError / finalize / retry / retryWhen

>>:  第29天-CSS-影像-(3-3)

Day31. 单例模式

本文同步更新於blog Singleton Pattern 确保一个类只有一个实例,并提供一个全局...

资视就是力量 - Highcharts / Vue 资料绑定

昨天我们成功安装 Highcharts-Vue 并绘制出一个基本的图表,不过既然都已经使用 Vue ...

@Day27 | C# WixToolset + WPF 帅到不行的安装包 [额外的DLL引用]

套件 在我们写程序时,一定会用很多来支援,让我们更轻松, 尤其是WPF的部分,为了漂亮肯定用了很多套...

伸缩自如的Flask [day2] blue_print

开始之前,我相信你已经有碰过flask的经验,或是至少知道藉由 pip install Flask ...

[SAP][PP]计划单转工单_BOM问题?

请教各位有没有碰过以下问题 主件料号多阶BOM如下 1.主件料号 2.半成品A(F) 2.半成品B(...