想象你有一个名叫 Adder 的外星仆人,它会跟随你到处走。除了作为一个可爱的外星伴侣,Adder 真正能做的一件事就是将两个数字相加。
为了验证 Adder 将两个数字相加的能力,我们可以生成一组测试用例,看看 Adder 是否给出正确答案。
在 Jasmine 中,这将从所谓的“suite”(测试套件)开始,它通过调用 describe 函数来分组相关的一组测试。
// 一个 Jasmine 测试套件
describe('Adder', () => {
});
从这里开始,我们可以为 Adder 提供一组测试用例,例如两个正数 (2, 4)、一个正数和零 (3, 0)、一个正数和一个负数 (5, -2) 等。
在 Jasmine 中,这些被称为“specs”(测试规范),我们通过调用 it 函数来创建它们,并传递一个字符串来描述正在测试的功能。
describe('Adder', () => {
// 一个 Jasmine 测试规范
it('should be able to add two whole numbers', () => {
expect(Adder.add(2, 2)).toEqual(4);
});
it('should be able to add a whole number and a negative number', () => {
expect(移除 AppComponent 测试
由于我们不再需要 AppComponent 的默认内容,让我们通过移除一些不必要的代码来更新它。
首先,移除 app.component.html 中的所有内容,只保留 router-outlet 指令。
<router-outlet></router-outlet>
在 app.component.ts 中,你也可以移除 title 属性。
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent { }
最后,你可以更新 app.component.spec.ts 中的测试,移除之前在 app.component.html 中的某些内容的两个测试。
import { async, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
});
测试 Angular service
现在我们的首页已经设置好了,我们可以着手创建一个 service 来用员工目录填充这个页面。
ng g service services/users/users
在这里,我们在新的 services/users 目录中创建了 users service,以将我们的 service 与默认的 app 目录分开,避免 app 目录快速变得杂乱。
现在我们的 service 已创建,我们可以对测试文件 services/users/users.service.spec.ts 进行一些小的修改。
我个人觉得在 it() 中注入依赖有些重复且难以阅读,默认的脚手架就是这样做的,如下面所示:
it('should be created', inject([TestService], (service: TestService) => {
expect(service).toBeTruthy();
}));
通过几处小的修改,我们可以将这个注入移动到 beforeEach 中,消除每个 it 中的重复代码。
import { TestBed } from '@angular/core/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let usersService: UsersService; // 添加这一行
beforeEach(() => {
TestBed.configureTestingModule({
providers: [UsersService]
});
usersService = TestBed.get(UsersService); // 添加这一行
});
it('should be created', () => { // 移除 inject()
expect(usersService).toBeTruthy();
});
});
在上方的代码中,TestBed.configureTestingModule({}) 通过在 providers 中设置 UsersService 来设置我们要测试的 service。然后我们使用 TestBed.get() 将 service 注入到测试套件中,参数是我们要测试的 service。我们将返回值设置到本地的 usersService 变量中,这将允许我们在测试中像在 component 中一样与这个 service 交互。
现在我们的测试设置已经重组完成,我们可以为返回用户集合的 all 方法添加一个测试。
import { of } from 'rxjs'; // 添加 import
describe('UsersService', () => {
...
it('should be created', () => {
expect(usersService).toBeTruthy();
});
// 为 all() 方法添加测试
describe('all', () => {
it('should return a collection of users', () => {
const userResponse = [
{
id: '1',
name: 'Jane',
role: 'Designer',
pokemon: 'Blastoise'
},
{
id: '2',
name: 'Bob',
role: 'Developer',
pokemon: 'Charizard'
}
];
let response;
spyOn(usersService, 'all').and.returnValue(of(userResponse));
usersService.all().subscribe(res => {
response = res;
});
expect(response).toEqual(userResponse);
});
});
});
在这里,我们为 all 将返回用户集合的期望添加了一个测试。我们声明了一个 userResponse 变量,设置为我们 service 方法的模拟响应。然后我们使用 spyOn() 方法来监视 usersService.all,并链式调用 .returnValue() 来返回我们的模拟 userResponse 变量,用 of() 包装它以作为 observable 返回这个值。
设置好 spy 后,我们像在 component 中一样调用 service 方法,订阅 observable,并将其返回值设置到 response。
最后,我们添加期望,response 将被设置为 service 调用返回的值 userResponse。
为什么要 mock?
此时,许多人会问:“为什么要 mock 响应?”为什么我们要为测试提供一个自己创建的返回值 userResponse,来手动设置服务返回的内容?服务调用不应该返回服务中的真实响应吗,无论是硬编码的用户集还是 HTTP 请求的响应?
这是一个完全合理的问题,当你刚开始测试时,可能很难理解这个概念。我发现用现实世界的例子来阐释这个概念是最容易的。
想象你拥有一家餐厅,开业前一晚。你召集所有雇员进行餐厅的“测试运行”。你邀请几位朋友进来,假装他们是顾客,坐下点餐。
在测试运行中,不会实际上菜。你已经和厨师合作过,确认他们能正确制作菜肴。在这次测试运行中,你想测试从顾客点菜,到服务员将订单发送到厨房,然后服务员履行厨房对顾客的响应的整个流程。厨房的响应可能有几种选项。
- 餐点已准备好。
- 餐点延迟。
- 无法制作餐点。我们缺少该菜的食材。
如果餐点准备好了,服务员会将餐点送到顾客面前。但是,如果餐点延迟或无法制作,服务员需要返回顾客身边,道歉,并可能询问是否需要第二道菜。
在测试运行中,当我们想测试前台(服务员)处理来自后端(厨房)请求的能力时,实际制作餐点是没有意义的。更重要的是,如果我们想测试服务员在餐点延迟或无法制作时的道歉能力,我们就必须等到厨师太慢或食材用尽,才能确认这些情况的测试。这显然不合理。因此,我们会“mock”后端(厨房),给服务员提供我们想要测试的各种场景。
同样,在代码中,当测试各种场景时,我们不会实际调用 API。我们 mock 可能收到的响应,并验证我们的应用能相应地处理该响应。就像厨房例子一样,如果我们测试应用处理 API 调用失败的能力,我们就必须等到 API 实际失败才能验证——希望这种场景不会经常发生!
添加用户
要让这个测试通过,我们需要在 users.service.ts 中实现服务方法。
首先,我们在服务中添加导入和员工集合。
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs'; // 添加导入
@Injectable({
providedIn: 'root'
})
export class UsersService {
users: Array<object> = [ // 添加员工对象
{
id: '1',
name: 'Jane',
role: 'Designer',
pokemon: 'Blastoise'
},
{
id: '2',
name: 'Bob',
role: 'Developer',
pokemon: 'Charizard'
},
{
id: '3',
name: 'Jim',
role: 'Developer',
pokemon: 'Venusaur'
},
{
id: '4',
name: 'Adam',
role: 'Designer',
pokemon: 'Yoshi'
}
];
constructor() { }
}
然后,在构造函数下方,我们可以实现 all 方法。
all(): Observable<Array<object>> {
return of(this.users);
}
再次运行 ng test,现在你应该看到包括我们服务方法的新测试都通过了。
将用户添加到首页
现在我们的服务方法已经准备就绪,我们可以开始将这些用户填充到首页。
首先,我们将使用 Bulma 更新 index.html,以帮助我们进行一些样式设计。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AngularTesting</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<!--添加这些-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.1/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.1.0/js/all.js"></script>
</head>
<body>
<app-root></app-root>
</body>
</html>
然后在 home/home.component.ts 中,我们可以添加对新服务的调用。
import { Component, OnInit } from '@angular/core';
import { UsersService } from '../services/users/users.service';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
users;
constructor(private usersService: UsersService) { }
ngOnInit() {
this.usersService.all().subscribe(res => {
this.users = res;
});
}
}
首先,我们导入服务并将其注入到组件的 constructor 中。然后在 ngOnInit 中添加对服务方法的调用,并将返回值设置到组件的 users 属性。
为了将这些用户显示到视图中,更新 home/home.component.html 中的模板。
<section class="section is-small">
<div class="container">
<div class="columns">
<div class="column" *ngFor="let user of users">
<div class="box">
<div class="content">
<p class="has-text-centered is-size-5">{% raw %}{{user.name}}{% endraw %}</p>
<ul>
<li><strong>Role:</strong> {% raw %}{{user.role}}{% endraw %}</li>
<li><strong>Pokemon:</strong> {% raw %}{{user.pokemon}}{% endraw %}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
现在当你运行 ng serve 并查看首页时,你应该会看到用户显示在 Bulma boxes 中。
查找单个用户
现在我们的用户已经填充到主页中,我们将添加另一个 service 方法,用于查找单个用户,该方法将用于用户资料页面。
首先,我们为新的 service 方法添加测试。
describe('all', () => {
...
});
describe('findOne', () => {
it('should return a single user', () => {
const userResponse = {
id: '2',
name: 'Bob',
role: 'Developer',
pokemon: 'Charizard'
};
let response;
spyOn(usersService, 'findOne').and.returnValue(of(userResponse));
usersService.findOne('2').subscribe(res => {
response = res;
});
expect(response).toEqual(userResponse);
});
});
在这里,我们添加了一个测试,期望 findOne 返回单个用户。我们声明了一个 userResponse 变量,设置为 service 方法的模拟响应,即用户集合中的单个对象。
然后,我们为 usersService.findOne 创建了一个 spy,并返回我们的模拟 userResponse 变量。设置好 spy 后,我们调用 service 方法并将其返回值设置为 response。
最后,我们添加断言,确认 response 被设置为 service 调用返回值 userResponse。
为了让这个测试通过,我们可以在 users.service.ts 中添加以下实现。
all(): Observable<Array<object>> {
return of(this.users);
}
findOne(id: string): Observable<object> {
const user = this.users.find((u: any) => {
return u.id === id;
});
return of(user);
}
现在当你运行 ng test 时,你应该会看到所有测试都处于通过状态。
结论
此时,我希望测试、它的好处以及编写测试的原因开始变得更加清晰。就我个人而言,我曾经很长时间推迟编写测试,主要原因是我不理解背后的原因,而且测试资源有限。
本教程中创建的应用在视觉上并非最令人印象深刻,但这是朝着正确方向迈出的一步。
在下一个教程中,我们将创建用户资料页面和一个使用 Pokeapi 获取 Pokemon 图片的 service。我们将学习如何测试发起 HTTP 请求的 service 方法以及如何测试 components。
额外内容
如果你希望测试在终端中以更易读的格式显示,有一个 npm 包可以做到这一点。
首先,安装该包。
npm install karma-spec-reporter --save-dev
安装完成后,打开 src/karma.conf.js,将新包添加到 plugins 中,并将 reporters 中的 progress 值更新为 spec。
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
require('karma-spec-reporter') // 添加此行
],
client: {
clearContext: false // 保留 Jasmine Spec Runner 输出在浏览器中可见
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['spec', 'kjhtml'], // 将 progress 更新为 spec
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
现在当你运行 ng test 时,你应该会看到测试套件有更美观的输出。
感谢与 Community 一起学习。请查看我们提供的计算、存储、网络和管理数据库产品。
了解更多我们的产品