编辑推荐: |
本文来自于csdn,本文主要介绍了测试的分类,ATDD,TDD,BDD,DDD的区别以及Karma、jasmine等工具的介绍等相关内容。 |
|
Angular_单元测试
测试分类
按开发阶段划分
单元测试
单元测试又称模块测试,针对软件设计中的最小单位——程序模块,进行正确性检查的测试工作。
集成测试
集成测试又叫组装测试,通常在单元测试的基础上,将所有程序模块进行有序的、递增测试。重点测试不同模块的接口部分
系统测试
指的是将整个软件系统看成一个整体进行测试,包括对功能、性能以及软件所运行的软硬件环境进行测试。
验收测试
指按照项目任务书或合同、供需双方约定的验收依据文档进行的对整个系统的测试与评审,决定是否接收或拒收系统
按是否运行划分
静态测试
是指不实际运行被测软件,而只是静态地检查程序代码、界面或文档中可能存在的错误过程
动态测试
是指实际运行被测程序,输入相应的测试数据,检查实际输出结果和预期结果是否相符的过程。
按是否查看源代码划分
黑盒测试
指的是把被测的软件看做一个黑盒子,不关心盒子里面的结构是什么样子,只关心软件的输入数据和输出数据。
白盒测试
指的是把盒子打开,去研究里面的源代码和程序结构。
其他
回归测试
是指软件被修改后重新进行的测试,重复执行上一个版本测试时的用例,是为了保证对软件所做的修改没有引入新的错误而重复进行的测试。
冒烟测试
是指在对一个新版本进行系统大规模的测试之前,先验证一下软件的基本功能是否实现,是否具备可测性。
随机测试
是指测试中所有的输入数据都是随机生成的,其目的是模拟用户的真实操作,并发现一些边缘性的错误。
ATDD,TDD,BDD,DDD
ATDD
ATDD: Acceptance Test Driven Development(验收测试驱动开发)
TDD 只是开发人员的职责,通过单元测试用例来驱动功能代码的实现。在准备实施一个功能或特性之前,首先团队需要定义出期望的质量标准和验收细则,以明确而且达成共识的验收测试计划(包含一系列测试场景)来驱动开发人员的TDD实践和测试人员的测试脚本开发。面向开发人员,强调如何实现系统以及如何检验。
TDD
TDD: Test-driven development (测试驱动开发)
是一种使用自动化单元测试来推动软件设计并强制依赖关系解耦的技术。使用这种做法的结果是一套全面的单元测试,可随时运行,以提供软件可以正常工作的反馈。
测试驱动开发是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD的基本思路就是通过测试来推动整个开发的进行,但测试驱动开发并不只是单纯的测试工作,而是把需求分析,设计,质量控制量化的过程。TDD首先考虑使用需求(对象、功能、过程、接口等),主要是编写测试用例框架对功能的过程和接口进行设计,而测试框架可以持续进行验证。。
BDD
BDD:Behavior-Driven Development (行为驱动开发)
行为驱动开发是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。主要是从用户的需求出发,强调系统行为。BDD最初是由Dan
North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。
DDD
DDD:领域驱动开发(Domain Drive Design)
DDD指的是Domain Drive Design,也就是领域驱动开发,DDD实际上也是建立在这个基础之上,因为它关注的是Service层的设计,着重于业务的实现,将分析和设计结合起来,不再使他们处于分裂的状态,这对于我们正确完整的实现客户的需求,以及建立一个具有业务伸缩性的模型。
Angular单元测试
Unit Test(单元测试)
对正式的项目进行单元测试是必须的,如果选择使用TDD(测试驱动开发)方法,则无关紧要,否则使用它将会产生很多好处。
在本文中,我们首先简单地提到单元测试的好处,然后我们将创建一个Angular单元测试的完整示例,使用jasmine和karma。
Karma的介绍
Karma是Testacular的新名字,在2012年google开源了Testacular,2013年Testacular改名为Karma。Karma是一个让人感到非常神秘的名字,表示佛教中的缘分,因果报应,比Cassandra这种名字更让人猜不透!
Karma是一个基于Node.js的JavaScript测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到CI(Continuous
integration)工具,也可和其他代码编辑器一起使用。这个测试工具的一个强大特性就是,它可以监控(Watch)文件的变化,然后自行执行,通过console.log显示测试结果。
jasmine介绍
TDD(Test Driven Development)测试驱动开发,是敏捷开发中提出的最佳实践之一。jasmine很有意思的提出了BDD(Behavior
Driven Development)行为驱动开发.
测试驱动开发,对软件质量起到了规范性的控制。未写实现,先写测试,一度成为Java领域研发的圣经。随着Javascript兴起,功能越来越多,代码量越来越大,开发人员素质相差悬殊,真的有必要建立对代码的规范性控制。jasmine就是为团队合作而生。
Jasmine是一个用来编写Javascript测试的框架,它不依赖于任何其它的javascript框架,也不需要对DOM。它有拥有灵巧而明确的语法可以让你轻松的编写测试代码。
jasmine的结构很简单:
describe("A
suite", function() {
var foo;
beforeEach(function() {
foo = 0;
foo += 1;
});
afterEach(function() {
foo = 0;
});
it("contains spec with an expectation",
function() {
expect(true).toBe(true);
});
}) |
每个测试都在一个测试集中运行,Suite就是一个测试集,用describe函数封装。 Spec表示每个测试用例,用it函数封装。通过expect函数,作为程序断言来判断相等关系。setup过程用beforeEach函数封装,tearDown过程用afterEach封装。
单元测试的好处
我们先来看看我认为在解决方案中使用单元测试的主要原因…
改进实现的设计
开始编写一个功能而不给设计带来太多的思考是开发人员非常常见的错误。使用单元测试将强制思考并重新考虑设计,如果您使用TDD,则影响会更大。
允许重构
既然你已经有测试确保你所有的东西都能按预期工作,你可以很容易地添加对代码的修改,确保你没有添加任何错误。
添加新功能而不会破坏任何内容
当您添加新功能时,您可以运行测试以确保您不会破坏应用程序的任何其他部分。
还有更多,但这三个在任何项目上都是如此巨大的胜利,对于我来说,这些赢利是封闭式的。但如果你不相信,让我们再提几个。
测试是很好的文档。
测试使开发人员对他们的工作更有信心。
你可以说他们所有的好处都是以很高的成本来实现的,但是这完全是错误的。所有使用单元测试可能花费的时间与以后在您引入新功能或进行任何重构时要节省的时间相比将会很小。花在解决错误上的时间要比没有使用单元测试时大大缩短。
我们将创建一个使用Angular,Jasmine和Karma的应用程序的小而完整的例子。
这些是我们要谈论的一些事情:
解释一下工具Karma和Jasmine
解释karma配置
解释test文件
创建第一个简单的测试,介绍Jasmine和Angular测试功能
测试一个Angular form,介绍Jasmine和Angular测试功能
测试一个带服务的组件,介绍Angular测试功能
使用jasmine和karma创建一个Angular项目
正如Angular团队建议我们要用Angular cli来创建我们的应用程序。通过这样做,jasmine和karma的配置可以帮我们解决,比较方便。
安装angular-cli并创建一个新项目:
npm install -g @angular/cli
ng new UnitTest –routing
当你创建项目时,所有的依赖关系都会安装,包括你需要创建测试的所有东西。
"@types/jasmine":
"~2.8.6", "@types/jasminewd2":
"~2.0.3", "@types/node":
"~8.9.4", "codelyzer":
"~4.2.1", "jasmine-core":
"~2.99.1", "jasmine-spec-reporter":
"~4.2.1", "karma": "~1.7.1",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter":
"~1.4.2", "karma-jasmine":
"~1.1.1",
"karma-jasmine-html-reporter": "^0.2.2", |
jasmine-core:Jasmine是我们将用来创建测试的框架。它有许多功能可以让我们编写不同类型的测试。
karma:Karma是我们测试的任务跑步者。它使用配置文件来设置启动文件,报告,测试框架,浏览器等等。
其余依赖主要为记录我们的测试,工具使用karma和jasmine和browser的发射器。
要运行测试,只需运行命令“ng test”。该命令将执行测试,打开浏览器,显示控制台和浏览器报告,同样重要的是,将测试执行保留为监视模式。也就是当我们修改过后,可以自动更新测试结果。
提示:如果想要终止,需要在终端内按CTRL+C
Karma配置
让我们来看看由angular-cli创建的karma配置文件。
// Karma configuration
file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
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')
],
client: {
clearContext: false // leave Jasmine Spec Runner
output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../coverage'),
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
|
你大概可以猜到这些配置属性的大部分用途,但我们来看看其中的一些。
frameworks:这是jasmine被设定为测试框架的地方。如果你想使用另一个框架,这是做这件事的地方。
reporters:负责将测试结果告知给开发者。通常是将结果打印到控制台上,或者存入文件中
autoWatch:如果设置为true,则测试将以Watch模式运行。如果您更改任何测试并保存文件,测试将重新生成并重新运行。
browsers:这是您设置测试应该运行的浏览器的位置。默认情况下是chrome,但你可以安装和使用其他浏览器启动器。
Test.ts文件
karma的angular-cli配置使用文件“test.ts”作为应用程序测试的入口点。我们来看看这个文件;
// This file
is required by karma.conf.js and loads recursively
all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
|
你可能永远不需要改变这个文件,但是有时候还是会更改的,比如:某一个spec文件排除测试等。
测试体验
我们来创建我们的第一个测试。修改app.component.ts。这个组件只有一个属性“text”,其值为“Angular
Unit Testing”,它是在HTML中的“h1”标记中呈现的,它还包含路由根元素和一些路由链接。让我们创建一个测试文件来检查组件是否实际具有该属性,并且实际上是在HTML中呈现的。
app.component.html文件
<h1>{{text}}</h1>
<router-outlet></router-outlet> |
app.component.ts文件
import { Component
} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
text = 'Angular Unit Testing';
} |
app.component.spec.ts文件
import { TestBed,
async } 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();
}));
it(`should have as title 'app'`, async(() =>
{
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.text).toEqual('Angular Unit Testing');
}));
it('should render title in a h1 tag', async(()
=> {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent) .toContain('Welcome
to Angular Unit Testing!');
}));
});
|
此时执行:
或
ng test的常用参数.
- –code-coverage -cc 代码覆盖率报告, 默认这个是不开启的, 因为生成报告的速度还是比较慢的.
- –colors 输出结果使用各种颜色 默认开启
- –single-run -sr 执行测试, 但是不检测文件变化 默认不开启
- –progress 把测试的过程输出到控制台 默认开启
- –sourcemaps -sm 生成sourcemaps 默认开启
- –watch -w 运行测试一次, 并且检测变化 默认开启
在弹出的chrome浏览器窗口中显示:
我们详细介绍一下这个测试代码
导入测试文件的所有依赖项
这里要注意,你在组件内使用的依赖,这里面同样需要导入,否则会无法运行。
使用describe开始我们的测试
describe是一个函数,Jasmine 就是使用 describe
全局函数来测试的。
declare function
describe(description: string, specDefinitions:
() => void): void; |
表示分组类似测试套,也就是一组测试用例,支持description嵌套。
例子:
describe('测试显示/隐藏筛选条件',
()=>{
}) |
我们在每个之前使用异步。异步的目的是让所有可能的异步代码在继续之前完成
Jasmine 就是使用 it 全局函数来表示,和 describe 类似,字符串和方法两个参数。
每个 Spec 内包括多个 expectation 来测试需要测试的代码,只要任何一个
expectation 结果为 false 就表示该测试用例为失败状态。
describe('demo
test', () => {
const VALUE = true;
it('should be true', () => {
expect(VALUE).toBe(VALUE);
})
}); |
如果有很多需要测试的,可以多个it:
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();
}));
}); |
断言,使用 expect 全局函数来表示,只接收一个代表要测试的实际值,并且需要与 Matcher
代表期望值。
TestBed可以帮助我们创建app实例
代码中有3个it
第一个为异步测试app是否true或false
如果app是0;两次取反当然是false;
如果app是null;两次取反是false;
如果app是undefined;两次取法是false;
其余的,两次取反是true;
第二个为异步测试app是否有text属性,并且判断值是否和预期相同
第三个为异步测试app是否在h1标签中的显示值为预期值
测试Form
创建一个contact组件。
首先我们修改contact.component HTML文件
<div>
{{text}} </div> <form id="contact-form"
[formGroup]="contactForm" (ngSubmit)="onSubmit()"
novalidate> <div class="form-group">
<label class="center-block">Name:
<input class="form-control" formControlName="name">
</label> <label class="center-block">Email:
<input class="form-control" formControlName="email">
</label> <label class="center-block">Text:
<input class="form-control" formControlName="text">
</label> </div> <button
type="submit"
[disabled]="!contactForm.valid" class="btn
btn-success">Save</button>
</form> |
这很简单,针对代码不做任何解释了。
修改contact.component.ts文件
import { Component,
OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators }
from '@angular/forms';
@Component({
selector: 'app-contact',
templateUrl: './contact.component.html',
styleUrls: ['./contact.component.css']
})
export class ContactComponent {
text = 'contact page';
contactForm: FormGroup;
contact = {
name: '',
email: '',
text: ''
};
submitted = false;
constructor() {
this.createForm();
}
createForm(): void {
this.contactForm = new FormGroup({
'name': new FormControl(this.contact.name, [
Validators.required,
Validators.minLength(4)
]),
'email': new FormControl(this.contact.email, [
Validators.required,
Validators.email
]),
'text': new FormControl(this.contact.text, Validators.required)
});
}
onSubmit(): void {
this.submitted = true;
}
}
|
这个组件也很容易理解。 onSubmit提交函数只是将提交的属性更改为true。
修改app-routing.module.ts
import { ContactComponent
} from './contact/contact.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: 'contact',
pathMatch: 'full'
},
{
path: 'contact',
component: ContactComponent
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
|
此时终端执行:
修改测试文件contact.component.spec.ts
import { BrowserModule,
By } from '@angular/platform-browser';
import { async, ComponentFixture, TestBed } from
'@angular/core/testing';
import { ContactComponent } from './contact.component';
import { DebugElement } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from
'@angular/forms';
describe('ContactComponent', () => {
let comp: ContactComponent;
let fixture: ComponentFixture<ContactComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
ContactComponent
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule
]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(ContactComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('form'));
el = de.nativeElement;
});
}));
it(`should have as text 'contact page'`, async(()
=> {
expect(comp.text).toEqual('contact page');
}));
it('should set submitted to true', async(() =>
{
comp.onSubmit(); // 直接内部调用onSubmit函数, submitted被更改为true
expect(comp.submitted).toBeTruthy();
}));
it('form call the onSubmit method', async(() =>
{
fixture.detectChanges();
spyOn(comp, 'onSubmit');
el = fixture.debugElement.query(By.css('button')) .nativeElement;
el.click(); // 模拟在html界面上点击onSubmit,此时是不能被点击的,因为没有输入,所以次数应该是0
expect(comp.onSubmit).toHaveBeenCalledTimes(0);
}));
it('form should be invalid', async(() => {
comp.contactForm.controls['email'].setValue('');
comp.contactForm.controls['name'].setValue('');
comp.contactForm.controls['text'].setValue('');
expect(comp.contactForm.valid).toBeFalsy();
}));
it('form should be vaild', async(() => {
comp.contactForm.controls['email'].setValue ('asd@asd.com');
comp.contactForm.controls['name'].setValue('aada');
comp.contactForm.controls['text'].setValue('text');
expect(comp.contactForm.valid).toBeTruthy();
}));
});
|
此时执行:
我们来分析一下,这个测试文件做了哪些东西?
导入依赖模块BrowserModule,FormsModule,ReactiveFormsModule
使用”By”将DOM中的form导入进来
第一个测试text属性
测试onSubmit函数调用
第三个测试使用“fixture”对象的函数“detectChanges”将组件状态应用于HTML,然后从DOM获取提交按钮并触发单击事件。在此之前,我们在组件的“onSubmit”功能上创建一个jasmine
“spy”。最后,我们期望onSubmit函数不会被执行,因为这个按钮应该被禁用,因为表单无效。
第四个测试将无效值设置为组件表单,并期望表单有效属性为false。
最后,在第五个测试中,我们将有效值设置为表单并期望表单有效属性为真。
小提示
- detectChanges
在测试中的Angular变化检测。每个测试程序都通过调用fixture.detectChanges()来通知Angular执行变化检测。
- By
By类是Angular测试工具之一,它生成有用的predicate。 它的By.css静态方法产生标准CSS选择器
predicate,与JQuery选择器相同的方式过滤。
测试服务service
当你要测试一个带有服务的组件时,就像我们已经看到的那样,你需要将提供者添加到在“beforeEach”中创建的测试模块。事情是,你可能不想使用实际的服务,而是一个模拟版本,所以让我们看看如何做到这一点……
创建一个app.service服务
修改app.service.ts
import { Injectable
} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class AppService {
constructor() { }
getInfo(): string {
return 'test service';
}
} |
修改app.component.ts
import { AppService
} from './app.service';
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
text = 'Angular Unit Testing';
info: string;
constructor(private service: AppService) {
this.info = this.service.getInfo();
}
} |
在app.component.spec.ts内增加
注意引入AppService服务。
...
providers: [
AppService
]
...
it('should have as info test service', async(()
=> {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.info).toEqual('test service');
}));
|
修改app.service.spec.ts
import { TestBed,
inject, async } from '@angular/core/testing';
import { AppService } from './app.service';
describe('AppService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AppService]
});
});
it('should be created', inject([AppService], (service:
AppService) => {
expect(service).toBeTruthy();
}));
it('should getInfo test service', inject([AppService],
(service: AppService) => {
expect(service.getInfo()).toEqual('test service');
}));
}); |
此时执行:
测试全部通过。
小提示
有些时候我们希望不是异步的,这时需要使用takeAsync函数,fakeAsync最重要的好处是测试程序看起来像同步的。
it('should show
quote after getQuote promise (fakeAsync)', fakeAsync(()
=> {
fixture.detectChanges();
tick(); // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
}));
|
常用断言方法
Jasmine 提供非常丰富的API,一些常用的Matchers:
toBe() 等同 ===
toNotBe() 等同 !==
toBeDefined() 等同 !== undefined
toBeUndefined() 等同 === undefined
toBeNull() 等同 === null
toBeTruthy() 等同 !!obj
toBeFalsy() 等同 !obj
toBeLessThan() 等同 <
toBeGreaterThan() 等同 >
toEqual() 相当于 ==
toNotEqual() 相当于 !=
toContain() 相当于 indexOf
toBeCloseTo() 数值比较时定义精度,先四舍五入后再比较。
toHaveBeenCalled() 检查function是否被调用过
toHaveBeenCalledWith() 检查传入参数是否被作为参数调用过
toMatch() 等同 new RegExp().test()
toNotMatch() 等同 !new RegExp().test()
toThrow() 检查function是否会抛出一个错误
而这些API之前用 not 来表示负值的判断。
expect(true).not.toBe(false); |
这些Matchers几乎可以满足我们日常需求,当然你也可以定制自己的Matcher来实现特殊需求。
Mock
在实际的组件测试中发现组件往往依赖于服务。而服务又依赖于外部资源如http交互、本地资源等。为了屏蔽外部依赖方便组件的测试,可以对服务进行mock。对于服务的mock方式有两种:伪造服务实例(提供服务复制品)、刺探真实服务。这两种方式都能够达到mock的效果,我们可以挑选一种最适合自己当前测试文件的测试方式来进行测试。
Mock服务实例
第一步:编写服务的mock类
class TaskMonitorStubService
extends TaskMonitorService {
public queryTaskList(request: ViewTaskRequest):
Observable<any> {
return request.code === -1 ? Observable.of(runningTaskResponse):
Observable.of(finishedTashResponse)
}
}
|
第二步:在configureTestingModule用Mock的服务替换真实的服务
TestBed.configureTestingModule({
imports: [
HttpModule,
TaskMonitorModule
],
Providers: [
{provide: TaskMonitorService, useClass: TaskMonitorStubService}
]
})
|
刺探真实服务
Angular的服务都是通过注入器注入到系统中的,同样我们可以从根TestBed获取到注入服务的实例,然后结合刺探(Spy)对真实的服务的方法进行替换.
let taskMonitorService:
TaskMonitorService = TestBe.get(TaskMonitorService);
spyOn(taskMonitorService, 'queryTaskList').and.returnValue(Observable.of (runningTaskResponse)); |
|