前言
Angular其实有自己推荐的单元测试的工具:Karma + Jasmine,当创建Angular应用后,在package.json文件中已经添加了Karma和Jasmine的依赖性,同时Angular还提供了一些自己的测试工具类,比如TestBed,更多详情请参照(Angular - 测试)
karma介绍
-
Karma是由Google团队开发的一套前端测试运行框架,是个的
自动化测试管理工具,它不同于测试框架(Mocha,Jasmine,QUnit等),他是运行在这些测试框架之上。Karma是一直作为一个Test Runner而存在的。该工具可用于测试所有主流Web浏览器,也可集成到 CI (Continuous integration)工具,也可和其他代码编辑器一起使用,可监控文件的变化,自动执行测试 -
karma的基本运行过程:
- Karma启动一个web服务器,生成包含js源代码和js测试脚本的页面;
- 运行浏览器加载页面,并显示测试的结果;
- 如果开启检测,则当文件有修改时,执行继续执行以上过程。
- karma库的介绍
- karma:Karma核心组件
- karma-chrome-launcher:Chrome发射器,测试会在Chrome上执行
- karma-coverage-istanbul-reporter:coverage报告
- karma-jasmine:Jasmine核心组件
- karma-jasmine-html-reporter:Html测试报告
在src目录下会看到名为:karma.conf.js、test.ts的两个文件。
karma.conf.js:Karma的配置文件,其中需要重点关注的配置有:
-
frameworks:使用的测试框架,这里使用Jasmine
-
port:测试使用的端口
-
autoWatch:是否自动监测测试代码的改变,自动执行测试
-
plugins:测试使用到的插件,与package.json文件保持一致
-
browsers:测试运行使用的浏览器,这里使用Chrome,如果你需要使用其他浏览器,需要通过npm安装浏览器发射器,并在plugins和这里设置,例如使用Safari
test.ts:测试入口文件,其中初始化了测试环境以及指定所有测试文件
Jasmine介绍
Jasmine:用来编写Javascript测试的的框架,它不依赖于任何其它的javascript框架。
Angular提供的测试工具类
Angular 测试工具类帮助我们创建编写单元测试的环境,主要包括 TestBed 类和各种助手方法,都位于 @angular/core/testing 名称空间下。
TestBed 类是一个很重要的概念,他会创建一个测试模块,模拟一个正常的 Angular 模块的行为。我们可以通过 configureTestingModule 方法配置这个测试模块。
此外,我们可以使用 ComponentFixture 与组件及其元素进行交互;还可以使用 DebugElement 实现跨平台测试。
单元测试文件的基本组成
单元测试的结构通常由两部分组成:
- 测试用例(Test Spec):一段实际的单元测试代码。
- 测试集合(Test Suite):测试集合是测试用例的逻辑分组。例如,为某一个功能特征,创建一个测试集合,包含了所有相关的测试用例。
describe 方法定义了一个测试集合,第一个参数描述了集合的名称,第二个参数是一个箭头函数,包含了实际的测试用例。每一个测试用例,都是使用 it 方法定义的。
beforeEach/afterEach会在describe中的每个it的方法参数的调用前/后都会执行一次。粗暴的理解就是说在describe内执行一次或多次,在it内执行一次。 一个describe中可以有多个beforeEach
beforeAll/afterAll会在describe中的全部it的方法参数的调用前/后只执行一次。粗暴的理解就是说在describe内只执行一次。
beforexxx 一般是用来做一些数据的初始化
afterxxx 一般是自行调用从而清除代码,对于使用LocalStorage、SessionStorage保持的数据亦是如此
简单的示例
在app目录下,还会找到一个名为app.component.spec.ts的文件,这就是一个Jasmine的测试,内容如下:
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';//测试入口,参数为测试名、方法
describe('AppComponent', () => { //每个测试用的Setup
beforeEach(async(() => {
TestBed.configureTestingModule({ 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 'demo'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance; //断言,期望值是否满足要求
expect(app.title).toEqual('demo'); }));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement; //通过querySelector获取页面元素 expect(compiled.querySelector('h1').textContent).toContain('Welcome to test-demo!'); })); //每个测试用例的TearDown afterEach(function() { //清除测试数据 });});
在文件的开头,是一个
beforeEach初始化函数,完成测试前的准备工作。在beforeEach方法中,调用了TestBed类的configureTestingModule方法配置组件所在的模块。configureTestingModule方法接收一个对象参数,而对象参数的值与模块的@NgModule装饰器的值相同。在这里,我们传入的是declarations数组,包含了AppComponent组件。完成模块配置后,调用compileComponents完成测试模块的构建。一旦构建成功,AppComponent组件就属于该测试模块了。
在第一个单元测试用例中,使用
createComponent方法创建了一个AppComponent组件装置类的对象。该对象是一个ComponentFixture类的实例,泛型的类型值是AppComponent.ComponentFixture类专门用于组件的调试和测试, 在组件装置类的对象创建成功后,可以通过componentInstance属性,获得AppComponent组件类的实例,最后,我们使用toBeTruthy匹配函数,验证AppComponent实例是否有效。
因为我们已经获得了
AppComponent实例,就可以对他的公共属性和方法进行测试。在单元测试用例中,我们使用toEqual方法验证title属性对值。
我们先调用了 ComponentFixture 类的 detectChanges 方法,目的是触发组件的变化检测机制,强制更新绑定的数据。然后,再使用组件的 nativeElement 属性,查找绑定数据的 DOM 元素,最后,验证 DOM 元素的 textContent 是否与绑定的数据相同
在Jasmine中常用的工具接口如下:
interface Matchers<T> {
/**
* toBe: 真实的值和期待的值的 '===' 比较
* @param expected - 期望值
* @param expectationFailOutput
* @example
* expect(thing).toBe(realThing);
*/
toBe(expected: any, expectationFailOutput?: any): Promise<void>;
/**
* toEqual: 支持 object 的深度对比, (功能 > toBe )
* @param expected - 期望值
* @param expectationFailOutput
* @example
* expect(bigObject).toEqual({ "foo": ['bar', 'baz'] });
*/
toEqual(expected: any, expectationFailOutput?: any): Promise<void>;
/**
* toMatch: 通过正则表达式比较
* @param expected - 期望值
* @param expectationFailOutput
* @example
* expect("my string").toMatch(/string$/);
* expect("other string").toMatch("her");
*/
toMatch(expected: string | RegExp | Promise<string | RegExp>, expectationFailOutput?: any): Promise<void>;
/**
* toBeDefined: 判断是否定义,非 `undefined`
* @example
* var a = undefined;
* var b = '';
* var c = null;
* expect(a).toBeDefined(); // Error
* expect(b).toBeDefined(); // Ok
* expect(c).toBeDefined(); // Ok
*/
toBeDefined(expectationFailOutput?: any): Promise<void>;
/**
* toBeUndefined: 值为 `undefined`
* 与 toBeDefined 相反
*/
toBeUndefined(expectationFailOutput?: any): Promise<void>;
/**
* toBeNull: 值为 `null`
*/
toBeNull(expectationFailOutput?: any): Promise<void>;
/**
* toBeNaN: 值为 `NaN`
*/
toBeNaN(): Promise<void>;
/**
* toBeTruthy: 是否是真实有效的值(非 空字符串,undefined,null)
*/
toBeTruthy(expectationFailOutput?: any): Promise<void>;
/**
* toBeFalsy: 判断是否是false
* @example
* expect(result).toBeFalsy();
*/
toBeFalsy(expectationFailOutput?: any): Promise<void>;
/**
* toHaveBeenCalled: 判断函数是否被调用
* @example
*/
toHaveBeenCalled(): Promise<void>;
/**
* toHaveBeenCalledWith: 函数被调用时的参数
* @example
*/
toHaveBeenCalledWith(...params: any[]): Promise<void>;
/**
* toHaveBeenCalledTimes: 函数被调用的次数
* @example
*/
toHaveBeenCalledTimes(expected: number | Promise<number>): Promise<void>;
/**
* toContain: 判断是否含有指定值
* @example
* expect(array).toContain(anElement);
* expect(string).toContain(substring);
*/
toContain(expected: any, expectationFailOutput?: any): Promise<void>;
/**
* toBeLessThan: 小于
* @example
* var num = 2;
* expect(num).toBeLessThan(2); // Error: Expected 2 to be less than 2.
* expect(num).toBeLessThan(3); // Ok
*/
toBeLessThan(expected: number | Promise<number>, expectationFailOutput?: any): Promise<void>;
/**
* toBeLessThanOrEqual: 小于等于
*/
toBeLessThanOrEqual(expected: number | Promise<number>, expectationFailOutput?: any): Promise<void>;
/**
* toBeGreaterThan: 大于
*/
toBeGreaterThan(expected: number | Promise<number>, expectationFailOutput?: any): Promise<void>;
/**
* toBeGreaterThanOrEqual: 大于等于
*/
toBeGreaterThanOrEqual(expected: number | Promise<number>, expectationFailOutput?: any): Promise<void>;
/**
* toBeCloseTo: 判断是否相似
* @expected 预期值
* @precision 精度
* @example
* var num = 1.01
* expect(num).toBeCloseTo(1); // OK
* expect(num).toBeCloseTo(1, 1); // OK
* expect(num).toBeCloseTo(1, 2); // Error: Expected 1.01 to be close to 1, 2.
*/
toBeCloseTo(expected: number | Promise<number>, precision?: any, expectationFailOutput?: any): Promise<void>;
toThrow(expected?: any): Promise<void>;
toThrowError(message?: string | RegExp | Promise<string | RegExp>): Promise<void>;
toThrowError(expected?: new (...args: any[]) => Error | Promise<new (...args: any[]) => Error>, message?: string | RegExp | Promise<string | RegExp>): Promise<void>;
}
如何在Jasmine中做监测
- spyOn(obj, 'method') //监听obj对象下的method方法,设置监听之后我们可以通过以下三个方法做相应的验证
toHaveBeenCalled:如果调用了spy,则 toHaveBeenCalled 将通过调用。
toHaveBeenCalledWith: 如果参数列表与spy调用时的参数相匹配,则返回true。
toHaveBeenCalledTimes:如果spy被调用了指定的次数,则 toHaveBeenCalledTimes 将通过调用。
- createSpy('dummy') // 创建一个虚拟的监听对象,同样可以用上述三种方法做相应的校验
做监测时的注意事项
- spyon只是检测,并没有真正的调用某些属性或者方法
- 顺序很重要,要先监测然后在执行对应的方法调用,才能真正的监测到方法的调用
- 如果检验的函数是个private的,我们可以将对应obj改写成 obj as any,再去进行方法调用
测试异步内容(observable,promise)
async
async 无任何参数与返回值,所有包裹代码块里的测试代码,可以通过调用 whenStable() 让所有待处理异步行为都完成后再进行回调;最后,再进行断言操作。
it('should be get user info (async)', async(() => {
// call component.query();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(1).toBe(1);
});
}));
fakeAsync执行异步操作
fakeAsync不需要回调,就可以进行断言判断,当我们在测试中使用到fakeAsync来做异步操作,那么有两个配套的方法来执行对应的异步操作,分别是 flushMicrotasks and tick,tick(100)表示异步操作在100秒内会被执行,异步宏任务的时候选择使用tick,当用到微任务的时候选择使用flushMicrotasks
it('should be get user info (async)', fakeAsync(() => {
// call component.query();
tick();
fixture.detectChanges();
expect(1).toBe(1);
}));
jasmine自带的done方法
调用beforeEach, it和afterEach时,函数可以包含一个可选参数done,当spec执行完毕之后,调用done通知Jasmine异步操作已执行完毕
it("takes a long time", function(done) {
setTimeout(function() {
done();
}, 9000);
}, 10000);
jasmine.DEFAULT_TIMEOUT_INTERVAL
设置全局的默认超时时间,可以设置jasmine.DEFAULT_TIMEOUT_INTERVAL的值,当异步执行时间超过设置的执行超时时间js将会报错。
运行单元测试文件
在项目所在的路径下,打开一个命令行,运行测试命令 ng test,默认情况下会跑整个项目所有的测试用例,如果你想单独跑某个用例,你可以替换it为fit;如果你想跑整个单独的文件你可以替换describe为fdescribe.