3.3测试 (Testing) 3.3 测试 (Testing) 在 Angular 应用中,测试是保证代码质量、可维护性和可靠性的关键环节。良好的测试策略能够帮助我们尽早发现并修复错误,减少回归,并确保应用在不断迭代更新的过程中保持稳定。Angular 社区提供了强大的测试工具和框架,使得编写各种类型的测试变得更加容易。 3.3.1 测试类型 在 Angular 应用中,常见的测试类型包括: 单元测试 (Unit Testing): 针对应用中最小的可测试单元(通常是一个函数、方法或组件)进行测试。单元测试的目标是验证每个单元的逻辑是否正确,独立于其他单元。 集成测试 (Integration Testing): 测试应用中多个单元之间的交互和协作。
在 Angular 应用中,测试是保证代码质量、可维护性和可靠性的关键环节。良好的测试策略能够帮助我们尽早发现并修复错误,减少回归,并确保应用在不断迭代更新的过程中保持稳定。Angular 社区提供了强大的测试工具和框架,使得编写各种类型的测试变得更加容易。
在 Angular 应用中,常见的测试类型包括:
单元测试 (Unit Testing): 针对应用中最小的可测试单元(通常是一个函数、方法或组件)进行测试。单元测试的目标是验证每个单元的逻辑是否正确,独立于其他单元。
集成测试 (Integration Testing): 测试应用中多个单元之间的交互和协作。集成测试的目标是验证不同单元是否能够正确地协同工作,例如组件之间的数据传递、服务之间的调用等。
端到端测试 (End-to-End Testing, E2E): 模拟真实用户的使用场景,从用户界面的角度对整个应用进行测试。E2E 测试的目标是验证应用的整体功能是否符合预期,例如用户登录、数据提交、页面跳转等。
组件测试 (Component Testing): 组件测试是一种特殊的集成测试,它专注于测试单个 Angular 组件及其模板的交互。它通常使用组件测试框架来模拟用户交互和验证组件的输出。
Angular 社区提供了丰富的测试工具和框架,其中最常用的包括:
Jasmine: 一个流行的 JavaScript 测试框架,提供了丰富的断言和测试结构,易于使用和学习。
Karma: 一个测试运行器,可以启动浏览器并执行测试代码,支持多种浏览器和测试框架。
Protractor: 一个专门用于 Angular 应用的 E2E 测试框架,基于 WebDriver,可以模拟用户在浏览器中的行为。
Angular CLI: Angular CLI 集成了测试工具,方便创建、运行和管理测试。
Jest: 一个流行的 JavaScript 测试框架,具有快速、零配置和快照测试等优点。
使用 Angular CLI 创建一个新项目:
ng new angular-testing-example cd angular-testing-example
创建一个简单的服务:
ng generate service calculator
src/app/calculator.service.ts
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class CalculatorService { constructor() { } add(a: number, b: number): number { return a + b; } subtract(a: number, b: number): number { return a - b; } }
Angular CLI 已经为我们生成了 src/app/calculator.service.spec.ts 文件,我们可以在其中编写单元测试。
import { TestBed } from '@angular/core/testing'; import { CalculatorService } from './calculator.service'; describe('CalculatorService', () => { let service: CalculatorService; beforeEach(() => { TestBed.configureTestingModule({}); service = TestBed.inject(CalculatorService); }); it('should be created', () => { expect(service).toBeTruthy(); }); it('should add two numbers', () => { expect(service.add(2, 3)).toBe(5); }); it('should subtract two numbers', () => { expect(service.subtract(5, 2)).toBe(3); }); });
describe 函数用于定义一个测试套件,其中包含多个相关的测试用例。
beforeEach 函数用于在每个测试用例执行之前执行一些准备工作,例如创建服务实例。
it 函数用于定义一个测试用例,其中包含一个或多个断言。
expect 函数用于创建一个断言,验证代码的执行结果是否符合预期。
使用 Angular CLI 运行单元测试:
ng test
Karma 会启动浏览器并执行测试代码,并在终端中显示测试结果。
创建一个简单的组件:
ng generate component hello
src/app/hello/hello.component.ts
import { Component, Input } from '@angular/core'; @Component({ selector: 'app-hello', templateUrl: './hello.component.html', styleUrls: ['./hello.component.css'] }) export class HelloComponent { @Input() name: string = ''; }
src/app/hello/hello.component.html
<p>Hello, {{ name }}!</p>
Angular CLI 已经为我们生成了 src/app/hello/hello.component.spec.ts 文件,我们可以在其中编写组件测试。
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HelloComponent } from './hello.component'; import { By } from '@angular/platform-browser'; describe('HelloComponent', () => { let component: HelloComponent; let fixture: ComponentFixture<HelloComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ HelloComponent ] }) .compileComponents(); fixture = TestBed.createComponent(HelloComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should display the name', () => { component.name = 'World'; fixture.detectChanges(); const paragraph = fixture.debugElement.query(By.css('p')); expect(paragraph.nativeElement.textContent).toContain('Hello, World!'); }); });
TestBed.createComponent 函数用于创建一个组件实例,并返回一个 ComponentFixture 对象。
ComponentFixture 对象提供了访问组件实例、组件模板和组件宿主元素的方法。
fixture.detectChanges 函数用于触发 Angular 的变更检测,更新组件模板中的数据绑定。
fixture.debugElement.query 函数用于在组件模板中查找元素。
使用 Angular CLI 运行组件测试:
ng test
Karma 会启动浏览器并执行测试代码,并在终端中显示测试结果。
集成测试通常涉及测试组件之间的交互,或者服务与组件之间的交互。
例如,假设我们有一个 UserListComponent,它从 UserService 获取用户列表并显示出来。我们需要测试 UserListComponent 是否正确地调用了 UserService,并且正确地显示了用户列表。
首先,创建 UserService 和 UserListComponent:
ng generate service user ng generate component user-list
src/app/user.service.ts
import { Injectable } from '@angular/core'; import { of } from 'rxjs'; export interface User { id: number; name: string; } @Injectable({ providedIn: 'root' }) export class UserService { getUsers() { const users: User[] = [ { id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' } ]; return of(users); } }
src/app/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core'; import { UserService, User } from '../user.service'; @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.css'] }) export class UserListComponent implements OnInit { users: User[] = []; constructor(private userService: UserService) { } ngOnInit(): void { this.userService.getUsers().subscribe(users => { this.users = users; }); } }
src/app/user-list/user-list.component.html
<ul> <li *ngFor="let user of users">{{ user.name }}</li> </ul>
现在,编写集成测试:
src/app/user-list/user-list.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserListComponent } from './user-list.component'; import { UserService, User } from '../user.service'; import { of } from 'rxjs'; import { DebugElement } from '@angular/core'; import { By } from '@angular/platform-browser'; describe('UserListComponent', () => { let component: UserListComponent; let fixture: ComponentFixture<UserListComponent>; let userService: UserService; let getUsersSpy: jasmine.Spy; const mockUsers: User[] = [ { id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' } ]; beforeEach(async () => { const userServiceMock = jasmine.createSpyObj('UserService', ['getUsers']); await TestBed.configureTestingModule({ declarations: [ UserListComponent ], providers: [ { provide: UserService, useValue: userServiceMock } ] }) .compileComponents(); fixture = TestBed.createComponent(UserListComponent); component = fixture.componentInstance; userService = TestBed.inject(UserService); getUsersSpy = userServiceMock.getUsers.and.returnValue(of(mockUsers)); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should call userService.getUsers on init', () => { expect(getUsersSpy).toHaveBeenCalledTimes(1); }); it('should display the users', () => { const listItems: DebugElement[] = fixture.debugElement.queryAll(By.css('li')); expect(listItems.length).toBe(2); expect(listItems[0].nativeElement.textContent).toBe('John Doe'); expect(listItems[1].nativeElement.textContent).toBe('Jane Smith'); }); });
在这个测试中,我们使用了 jasmine.createSpyObj 创建了一个 UserService 的模拟对象。这样我们就可以控制 getUsers 方法的返回值,并且验证它是否被调用。
端到端测试使用 Protractor 或 Cypress 等工具来模拟用户在浏览器中的行为。由于 E2E 测试涉及整个应用的运行,因此通常需要一个运行中的 Angular 应用。
确保已经安装了 Protractor:
npm install -g protractor webdriver-manager update
Angular CLI 已经为我们生成了 e2e/src/app.e2e-spec.ts 文件,我们可以在其中编写 E2E 测试。
import { AppPage } from './app.po'; import { browser, logging } from 'protractor'; describe('workspace-project App', () => { let page: AppPage; beforeEach(() => { page = new AppPage(); }); it('should display welcome message', async () => { await page.navigateTo(); expect(await page.getTitleText()).toEqual('angular-testing-example app is running!'); }); afterEach(async () => { // Assert that there are no errors emitted from the browser const logs = await browser.manage().logs().get(logging.Type.BROWSER); expect(logs).not.toContain(jasmine.objectContaining({ level: logging.Level.SEVERE, } as logging.Entry)); }); });
首先,启动 Angular 应用:
ng serve
然后,运行 E2E 测试:
ng e2e
Protractor 会启动浏览器并执行测试代码,并在终端中显示测试结果。
尽早开始测试: 在开发过程中尽早开始编写测试,可以帮助我们尽早发现并修复错误。
编写清晰的测试用例: 测试用例应该清晰、简洁、易于理解和维护。
覆盖所有重要的代码路径: 测试用例应该覆盖所有重要的代码路径,确保代码的正确性。
使用 Mock 和 Stub: 在单元测试和集成测试中,可以使用 Mock 和 Stub 来模拟外部依赖,隔离被测试单元。
持续集成: 将测试集成到持续集成流程中,可以确保每次代码提交都会自动运行测试,及时发现并修复错误。
测试驱动开发 (TDD): 在编写实际代码之前先编写测试用例,有助于更好地理解需求,并确保代码的质量。
测试是 Angular 应用开发中不可或缺的一部分。通过编写各种类型的测试,我们可以确保代码的质量、可维护性和可靠性。Angular 社区提供了强大的测试工具和框架,使得编写测试变得更加容易。 掌握 Angular 的测试技术,可以帮助我们构建更加健壮和可靠的应用。