Karma与TSLint
TSLint
TSLint是一个可扩展的静态分析工具,用于检查TypeScript代码的可读性,可维护性和功能性错误。收到现代编辑和构建系统的广泛支持,并且可以使用您自己的路由,配置和格式化。
安装
npm install tslint typescript -g
运行linter之前,请确保TypeScript源文件能够正确编译。
用法 tslint [options] [file ...]
-v, --version 输出版本号
-c, --config [config] 配置文件
-e, --exclude <exclude> 从路径扩展中全局排除
--fix 修正了选择规则的链接错误(这可能会覆盖linted文件)
--force 返回状态码0,即使有lint错误
-i, --init 生成一个tslint。当前工作目录中的json配置文件
-o, --out [out] 输出文件
--outputAbsolutePaths 输出文件路径是否是绝对路径
-r, --rules-dir [rules-dir] 规则目录
-s, --formatters-dir [formatters-dir] 格式器目录
-t, --format [format] 输出格式(散文、json、时髦、冗长、pmd、msbuild、checkstyle、vso、文件列表、codeFrame)
--test 测试tslint为指定的目录生成正确的输出
-p, --project [project] tsconfig。json文件
-h, --help 输出使用信息
默认情况下,TSLint将查找tslint.json正在创建的文件的目录中指定的配置文件,如果未找到,则搜索祖先目录。查看规则部分,了解有关可用规则的更多详细信息。
tslint接受以下命令行选项
-c, --config:
tslint将使用配置文件的位置来确定哪些规则被激活,以及哪些选项可以 提供给规则。如果没有指定选项,配置文件名为tslint。使用json,只要它存在于路径中。文件的格式是规则:/规则列表/,其中/规则列表/是一个键:值逗号分隔的rulename列表:规则选项对。规则选项可以是布尔true/false值,表示规则是否被使用,或者是一个布尔值,……布尔值提供与非列表情况相同的角色,而列表的其余部分则是传递给规则的选项,该规则将决定它所检查的内容(例如,max-line长度规则的字符数,或者禁止禁止规则的功能)。 -e, --exclude:
一个文件名或glob,表示从linting中排除文件。如果您需要多个选项,可以多次提供此选项 globs表示要排除哪些文件。 --fix:
修正了选择规则的链接错误。这可能会覆盖linted文件。 --force:
返回状态码0,即使有任何lint错误。
作为npm脚本运行时很有用。 -i, --init:
生成一个tslint。当前工作目录中的json配置文件。 -o, --out:
将结果输出到的文件名。默认情况下,tslint输出到stdout,通常是您运行它的控制台。 --outputAbsolutePaths:
如果是真的,输出中的所有路径都是绝对的。 -r, --rules-dir:
另一个规则目录,用于用户创建的规则。
tslint将始终检查其默认规则目录
在检查用户提供的信息之前,节点模块/tslint/lib/规则
规则目录,所以用户提供的规则目录中的规则
与基本规则相同的名称将不会被加载。 -s, --formatters-dir:
为用户创建的格式化程序,另一个格式化目录。
格式化程序是将格式化tslint输出的文件
把它写在stdout或文件中——out。默认的
目录,节点模块/tslint/构建/格式化程序,将永远是
首先检查,所以用户创建的表单具有相同的名称
因为基础格式化程序将不会被加载。 -t, --format:
格式化程序用于格式化linter的结果
把它输出到stdout或者文件中传递出去。的核心
格式器是散文(人类可读)、json(机器可读)
和详细。如果不使用此选项,则散文是默认的。
其他内置选项包括pmd、msbuild、checkstyle和vso。
如果——formatters-dir,可以添加和使用额外的格式化程序
选项设置。 --test:
在匹配的目录上运行tslint,并检查tslint输出
匹配.lint文件中的预期输出。自动加载
tslint。作为配置文件的目录中的json文件
测试。请参阅完整的tslint文档了解更多关于如何
这可以用来测试定制规则。 -p, --project:
包含tsconfig的路径或目录。将会是json文件
用于确定哪些文件将被连接。这个标志还使
需要类型检查器的规则。 -v, --version:
当前版本的tslint。 -h, --help:
打印此帮助消息。
TSLint 代码规则
https://palantir.github.io/tslint/rules/
测试
编写测试程序来探索和确认应用的行为。测试的作用有:
1. 测试守护由于代码变化而打破已有代码(“回归”)的情况。
2. 不管代码被正确使用还是错误使用,测试程序起到澄清代码的作用。
3. 测试程序暴露设计和实现可能出现的错误,测试程序从很多角度为代码亮出警示灯
工具与技术
可以用多种工具和技术来编写运行Angular测试程序。
Jasmine
测试框架提供所有编写基本测试的工具,自带HTML测试运行器,用来在浏览器中执行测试程序
Angular测试工具
为被测试的Angular应用代码创建测试环境。在应用代码与Angular环境互动时,来限制和控制应用的部分代码
Karma
在开发应用的过程中编写和运行单元测试的理想工具,能成为项目开发和连续一体化进程的不可分割的一部分。
Protractor
编写和运行e2e端对端的程序,端对端程序像用户体验应用程序那样探索它。
在测试中一条进程运行真正的应用,另一条运行Protractor测试程序,模拟用户行为,判断应用在浏览器中的反应是否正确。
独立单元测试.vs.Angular测试工具集
独立单元测试用于测试那些完全不依赖Angular或不需要注入值的类实例。
测试程序员会new出一个测试类的实例,为构造函数参数提供所需的测试替身,然后测试该实例的API接口。
我们应该为管道和服务书写独立的单元测试
也可以同样为组件写独立单元测试,不过独立单元测试无法体现组件与Angular交互。
具体来说,就是不能发现组件类如何与它的模板或其他组件交互。
这时就需要Angular测试工具集,包括TestBed类和一些来自@angular/core/testing的助手函数。
第一个Karma测试
创建1st.spec.ts。用Jasmine编写的测试程序都被叫做specs,文件名后缀必须是.spec.ts。
这是karma.conf.js和其他工具所坚持和遵循的规约。
将测试程序spec放到app文件夹下的任意位置,karma.conf.js告诉Karma在这个文件夹下找测试程序文件
describe('1st tests', () => {
it('true is true', () => expect(true).toBe(true));
});
运行:npm test,该命令编译应用及其测试代码,启动Karma,两个进程都监视相关文件,往控制台输入信息和检测变化时自动重新运行
Karma会打开浏览器并开始向控制台输出,查看控制台的输出,大致如下
> npm test
...
[0] 1:37:03 PM - Compilation complete. Watching for file changes.
...
[1] Chrome 51.0.2704: Executed 0 of 0 SUCCESS
Chrome 51.0.2704: Executed 1 of 1 SUCCESS
SUCCESS (0.005 secs / 0.005 secs)
编译器和Karma都在运行,信息[0]是编译器输入,[1]是Karma的输入
调试测试程序
在浏览器中,像调试应用一样调试测试程序spec。
显示Karma
的浏览器窗口(之前被隐藏了)。
点击“DEBUG”按钮;它打开一页新浏览器标签并重新开始运行测试程序
打开浏览器的“Developer Tools”(Windows上的Ctrl-Shift-I或者OSX上的`Command-Option-I)。
选择“sources”页
打开1st.spec.ts
测试文件(Control/Command-P, 然后输入文件名字)。
在测试程序中设置断点。
刷新浏览器...然后它就会停在断点上。
测试一个组件
大多数开发人员首先要测试的就是Angular组件,首先我们创建一个组件banner-inline.component.ts
import { Component } from '@angular/core'; @Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
我们推荐将单元测试的spec配置文件放到与应用程序源代码文件所在的同一个文件夹中,因为:
这样的测试程序很容易被找到
你可以一眼看出应用程序的那些部分缺乏测试程序。
临近的测试程序可以展示代码是如何在上下文中工作的
当你移动代码(无可避免)时,你记得一起移动测试程序
当你重命名源代码文件(无可避免),你记得重命名测试程序文件。
什么时候我应该把测试spec文件放到测试目录中?
应用程序的整合测试spec文件可以测试横跨多个目录和模块的多个部分之间的互动。 它们不属于任何部分,很自然,没有特别的地方存放它们。
通常,在test
目录中为它们创建一个合适的目录比较好。
当然,测试助手对象的测试spec文件也属于test
目录,与它们对应的助手文件相邻。
组件对应的测试代码banner-inline.component.spec.ts放在同一目录下
import {BannerComponent} from "./banner-inline.component";
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {DebugElement} from "@angular/core";
import {By} from "@angular/platform-browser"; describe('BannerComponent (inline template)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement; beforeEach(() => {
TestBed.configureTestingModule({
declarations: [BannerComponent], // declare the test component
}); fixture = TestBed.createComponent(BannerComponent); comp = fixture.componentInstance; // BannerComponent test instance // query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
});
TestBed测试台
TestBed测试台是Angular测试工具集中的首要概念,他创建Angular测试模块(一个NgModule)
可以通过调用它的configureTestingModule方法来为要测试的类生成模块环境。
其效果是,可以把被测试的组件从原有的应用模块中剥离出来,附件到一个动态生成的Angular测试模块上。
而该测试模块可以为这些测试进行特殊剪裁。
configureTestingModule方法接受一个类似@NgModule的元数据对象,这个元数据对象具有标准的Angular模块的大多数属性。
这里的元数据对象只是声明了要测试的组件BannerComponent。这个元数据中没有imports属性因为:
1.默认的测试模块配置中已经有了BannerComponent所需的一切
2.BannerComponent不需要与任何其他组件交互
在beforeEach中调用configureTestingModule,以便TestBed可以在运行每个测试之前都把自己重置会它的基础状态
基础状态中包含一个默认的测试模块配置,它包含每个测试都需要的那些声明(组件、指令和管道)以及服务提供商
之前提到的测试垫片初始化测试模块配置到一个模块,这个模块和@angular/platform-browser中的BrowserModule类似
这个默认的配置只是测试的基础性工作,稍后我们会调用TestBed.configureTestingModule来传入更多元数据,
这些元数据定义了额外的imports、declarations、providers和适用于这些测试的概要(Schema)
可选的override方法可以微调配置的各个方面
createComponent方法
在配置好TestBed之后,我们可以告诉他创建一个待测组件的实例,这个例子中
TestBed.createComponent创建了一个BannerComponet的实力,并返回一个ComponentFixture
调用了createComponent之后就不要再重新配置TestBed了
createComponent方法封闭了当前的TestBed实例,以免将来在配置它
我们不能再调用任何TestBed的方法修改配置:不能调用configureTestingModule或任何override方法。
如果这么做,TestBed就会抛出错误。
ComponentFixture、DebugElement、query(By.css)
createComponent方法返回ComponentFixtrue,用来控制和访问已创建的组件所在的测试环境。
这个fixture提供了对组件实例自身的访问,同时还提供了用来访问组件的DOM元素的DebugElement对象。
title属性被插值到DOM的<h1>标签中,用CSS选择器从fixture的DebugElement中query<h1>元素
query方法接受predicate函数,并搜索fixture的整个DOM树,试图寻找第一个满足predicate函数的元素。
queryAll方法返回一列数组,包含所有DebugElement中满足predicate的元素。
predicate是返回布尔值的函数,predicate查询接受DebugElement参数,如果元素符合选择条件便返回true。
By类是Angular测试工具之一,它生成游泳的predicate。它的By.css静态方法产生标准CSS选择器predicate
与JQuery选择器相同的方法过滤。
最后这个配置把DebugElement中的nativeElementDOM元素赋值给el属性,测试程序将判断el是否包含期待的标题文本。
测试程序
再每个测试程序之前,Jasmin都一次运行beforeEach函数。
import {BannerComponent} from './banner-inline.component';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser'; describe('BannerComponent (inline template)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement; beforeEach(() => {
TestBed.configureTestingModule({
declarations: [BannerComponent], // declare the test component
}); fixture = TestBed.createComponent(BannerComponent); comp = fixture.componentInstance; // BannerComponent test instance // query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
}); it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
}); it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
});
});
测试程序向DebugElement获取原生HTML元素,来满足自己的期望
detectChanges:在额始终的Angular变量检测
每个测试程序都通过调用fixture.detectChanges()来通知Angular执行变更检测。
第一个测试程序立刻这么做,出发数据绑定和并将title属性发送到DOM元素中。
第二个测试程序在更改组件的title属性之后才调用fixture.detectChanges,新值出现在DOM元素中。
产品阶段,当Angular创建组件、用户输入、异步动作完成时,自动触发变更检测。
TestBed.createComponent不会出发变更检测,该工具不会自动将组件的title属性值推送到数据绑定的元素
下面的测试程序展示了这个事实:
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
这种行为是有意为之,在Angular初始化数据绑定或者调用生命周期钩子之前,它给测试者机会来查看或改变组件的状态。
自动变更检测
BannerComponent的测试频繁调用detectChanges,有些测试人员更希望Angular的测试环境自动进行变更检查。
这可以通过为TestBed配置上ComponentFixtureAutoDetect提供商来做到。
首先从测试工具库中导入它:创建banner.component.detect-changes.spec.ts
然后添加测试模块配置providers数组
import {ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
import {BannerComponent} from './banner-inline.component'; describe('BannerDetectChanges', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [BannerComponent],
providers: [
{
provide: ComponentFixtureAutoDetect, useValue: true
}
]
});
});
});
下面测试阐明了自动变更检测的工作原理
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
import {BannerComponent} from './banner-inline.component';
import {DebugElement} from '@angular/core';
import {By} from '@angular/platform-browser'; describe('BannerDetectChanges', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement; beforeEach(() => {
TestBed.configureTestingModule({
declarations: [BannerComponent],
providers: [
{
provide: ComponentFixtureAutoDetect, useValue: true
}
]
}); fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
}); it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(el.textContent).toContain(comp.title);
}); it('should still see original title after comp.title change', () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
expect(el.textContent).toContain(oldTitle);
}); it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(el.textContent).toContain(comp.title);
});
});
第一个测试程序展示了自动检测的好处。
第二个和第三个测试程序展示了一个重要的局限性,Angular测试环境不会知道测试程序改变了组件的title属性。
自动检测只对异步行为比如承诺的解析、计时器和DOM事件作出反应。
但是直接修改组件属性值的这种同步更新是不会出发自动检测的,测试程序必须手动调用fixture.detectChange()
与其怀疑测试工具会不会执行变更检测,总是显式调用detectChanges(),即使在不需要的时候也没有任何坏处。
测试带有外部模块的组件
BannerComponent的行为和刚才的版本相同,但是实现方式不同,它有一个外部模板和CSS文件,通过templateUrl和styleUrls属性来指定。
创建banner.component
import { Component } from '@angular/core'; @Component({
selector: 'app-banner',
templateUrl: './banner.component.html',
styleUrls: ['./banner.component.css']
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
这些测试有一个问题,TestBed.createComponent方法是同步的。
但是Angular模板编译器必须在创建组件实例之前先从文件系统中读取这些值,而这是异步的。
以前测试内联模板时使用的设置方式不适用于外部模板。
异步的beforeEach
BannerComponet测试的设置方式必须给Angular模板编译器一些时间来读取文件。
以前放在beforeEach中的逻辑被拆分成两个beforeEach调用。
第一个beforeEach处理异步编译工作
import {async, TestBed} from '@angular/core/testing';
import {BannerComponent} from './banner.component'; beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [BannerComponent]
}).compileComponents();
}));
async函数被用作调用beforeEach的参数,async函数是Angular测试工具集的一部分,这里必须引入他。
它接受一个无参数的函数,并返回一个函数,这个函数会作为实参传给beforeEach
async参数的内容看起来非常像同步版beforeEach的函数体,并不能很明显的看出来这是异步函数。
内部实现上,async会把beforeEach的函数体放进一个特殊的异步测试区,隐藏了异步执行的内部机制。
这就是为了调用异步的TestBed.compileComponents方法所要做的一切。
compileComponents方法
TestBed.configureTestingModule方法返回TestBed类,以便你可以链式调用TestBed的其它静态方法,比如compileComponent
TestBed.compileComponents方法会异步编译这个测试模块中配置的所有组件。
BannerComponent是唯一要编译的组件,当compileComponents完成时,外部组件和css文件会被内联。
TestBed.createComponent会用同步的方式创建一个BannerComponent的新实例。
这个例子中,TestBed.compileComponents只会编译BannerComponent
所有这些组件都可能含有外部模板和css文件,TestBed.compileComponents会同时异步便宜所有这些声明的组件。
调用了compileComponents之后就不能再配置TestBed了,务必确保compileComponents是调用TestBed.createComponent
来实例化待测组件之前的最后一步。
compileComponents方法封闭了当前的TestBed实例,以免将来再配置它。
不能再调用任何TestBed的方法修改配置:不能调用configureTestingModule或任何override方法,否则会抛出错误。
同步beforeEach
在异步测试后,还需要用同步测试来完成接下来的步骤。
步骤与异步类似,测试运行器会先等待第一个异步beforeEach函数执行完成后再调用第二个
compileComponents会返回一个Promise,来让我们立即执行额外的任务,比如把第二个beforeEach放到回掉函数then里面。
但是因为不方便阅读,所以大部分还是写两个beforeEach调用的方式。
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core'; import { BannerComponent } from './banner.component'; describe('BannerComponent (templateUrl)', () => { let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement; // async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
})
.compileComponents(); // compile template and css
})); // synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent); comp = fixture.componentInstance; // BannerComponent test instance // query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
}); it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
}); it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
}); it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
}); });
测试有依赖的组件
组件经常依赖其他服务,例如下方组件
import { Component, OnInit } from '@angular/core'; import { UserService } from './user.service'; @Component({
selector: 'app-welcome',
template: '<h3 class="welcome" ><i>{{welcome}}</i></h3>'
})
export class WelcomeComponent implements OnInit {
welcome = '-- not initialized yet --';
constructor(private userService: UserService) { } ngOnInit(): void {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
其中UserService就是模拟作用,没有东西。下面看一下测试模块配置
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {WelcomeComponent} from "./welcome.component";
import {DebugElement} from "@angular/core";
import {UserService} from "./user.service"; describe('WelCome Spec', () => {
let comp: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
let de: DebugElement;
let el: HTMLElement; const userServiceStub = {
isLoggedIn: true,
user: {name: 'Test User'}
}; TestBed.configureTestingModule({
declarations: [WelcomeComponent],
providers: [{provide: UserService, useValue: userServiceStub}]
}); });
在测试配置中不但声明了被测试的组件,而且在providers数组添加了UserService依赖,但不是真实的UserService。
实际上,服务替身通常更合适进行测试。我们模拟了userService的服务替身。
获取注入服务,一共有两种方法
Injector
Angular的注入是层次化的,可以有很多层注入器,从根TestBed创建的注入器来贯穿整个组件树。
最安全有效的方法就是从被测试的组件的注入器获取,组件注入器是fixture的DebugElement的属性。
let userService = fixture.debugElement.injector.get(UserService);
TestBed.get
也可以通过TestBed.get方法来从根注入器中获取服务,更加简洁。但是只有Angular使用测试的根注入器中的那个服务实例来注入到组件时才有效
userService = TestBed.get(UserService);
获取注入服务其实是服务克隆,与传递过去的对象不一致
beforeEach(() => {
// stub UserService for test purposes
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
}; TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
providers: [ {provide: UserService, useValue: userServiceStub } ]
}); fixture = TestBed.createComponent(WelcomeComponent);
comp = fixture.componentInstance; // UserService from the root injector
userService = TestBed.get(UserService); // get the "welcome" element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.welcome'));
el = de.nativeElement;
});
it('should welcome the user', () => {
fixture.detectChanges();
const content = el.textContent;
expect(content).toContain('Welcome', '"Welcome ..."');
expect(content).toContain('Test User', 'expected name');
}); it('should welcome "Bubba"', () => {
userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
fixture.detectChanges();
expect(el.textContent).toContain('Bubba');
}); it('should request login if not logged in', () => {
userService.isLoggedIn = false; // welcome message hasn't been shown yet
fixture.detectChanges();
const content = el.textContent;
expect(content).not.toContain('Welcome', 'not welcomed');
expect(content).toMatch(/log in/i, '"log in"');
});
第一个测试程序是合法测试程序,确认这个被模拟的UserService是否被调用和工作正常。
第二个测试程序是验证变换用户名字的效果
第三个测试程序是检查如果用户没有登录,组件是否显示正确信息
测试异步服务组件
大部分数据服务向远程服务器发起HTTP请求,响应必须是异步的。
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent implements OnInit {
intervalId: number;
quote = '...';
constructor(private twainService: TwainService) { } ngOnInit(): void {
this.twainService.getQuote().then(quote => this.quote = quote);
}
}
ngOnInit的twainService.getQuote返回Promise,显然是异步操作。一般来讲不会真正去发送请求,而是仿真请求。
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [ TwainService ],
}); fixture = TestBed.createComponent(TwainComponent);
comp = fixture.componentInstance; // TwainService actually injected into the component
twainService = fixture.debugElement.injector.get(TwainService); // Setup spy on the `getQuote` method
spy = spyOn(twainService, 'getQuote')
.and.returnValue(Promise.resolve(testQuote)); // Get the Twain quote element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.twain'));
el = de.nativeElement;
});
与其伪造服务对象,它注入了真实的服务,并用Jasmine的spy替换关键的getQuote方法。称为:刺探(Spy)真实服务
spy设计是所有调用getQuote的方法都会收到立刻解析的承诺,得到一条预设的名言。spy拦截了实际getQuote方法,不会联系服务端。
it('should not show quote before OnInit', () => {
expect(el.textContent).toBe('', 'nothing displayed');
expect(spy.calls.any()).toBe(false, 'getQuote not yet called');
}); it('should still not show quote after component initialized', () => {
fixture.detectChanges();
// getQuote service is async => still has not returned with quote
expect(el.textContent).toBe('...', 'no quote yet');
expect(spy.calls.any()).toBe(true, 'getQuote called');
}); it('should show quote after getQuote promise (async)', async(() => {
fixture.detectChanges(); fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
});
})); 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);
}));
同步测试程序
前两个测试程序是同步的,在Spy的帮助下,验证了在Angular调用ngOnInit期间发生的第一次变更检测后,getQuote被调用了。
这两者都不能证明被显示的值是服务提供的,虽然spy返回了解析的承诺,但是内容还没有到来。
这个测试程序必须等待JavaScript引擎一整个回合,返回值才会有效,该测试程序必须要变成异步的。
注意第三个测试程序的async方法,async函数是Angular TestBed的一部分,通过将测试代码放在特殊的异步测试区域来运行。
async函数简化了异步测试程序的代码,会在beforeEach中被调用。
虽然async做了很多工作来尽量隐藏异步特性,但在测试程序中里面调用函数时,有时还是会体现他们的异步行为。
fakeAync可选方法,进一步移除了异步行为,提供了更加直观的代码经验。
whenStable方法
测试程序必须等待getQuote在JavaScript引擎的下一回合中被解析。
本测试对twainService.getQuote返回的承诺没有直接访问,因为它被埋没在TwainComponent.ngOnInit里
对于只测试组件API表面的测试来说,是无法被访问的。
异步测试区域可以访问getQuote承诺,因为它拦截所有调用异步方法所发出的承诺,不管他在那里。
ComponentFixture.whenStable方法返回它自己的承诺,在getQuote承诺完成时被解析。
stable的意思是当所有待处理的异步行为完成时的状态,在stable后whenStable承诺被解析。
然后测试程序继续运行,开始下一轮变更检测,通过Angular来更新DOM,getQuote辅助方法提取出显示元素的文本。
fakeAsync方法
第四个测试程序用不同的方法验证同样的组件行为。
在it参数中,async被fakeAsync替换,fakeAsync是另一种Angular测试工具
和async一样,接受无参数函数并返回一个函数,变成Jasmine的it函数的参数,通过特殊的测试区域运行测试程序,让代码更加简单直观。
tick函数
是Angular测试工具之一,只能在fakeAsync的主体中被调用。
调用tick()模拟时间的推移,直到全部待处理的异步任务都已完成,这个测试案例中,包含getQuote承诺的解析。
不返回任何结果,没有任何承诺需要等待,直接执行与之前相同的代码。
jasmine.done
虽然async和fakeAsync函数大大的简化了异步测试,你仍然可以回退到传统的Jasmine异步测试技术上。
仍然可以接受done回调的函数传给it。但是,你必须链接承诺、处理错误、并在适当的时候调用done
带有done回调的测试函数,适合涉及intervalTimer的代码或者异步Observable函数的场景。
it('should show quote after getQuote promise (done)', (done: any) => {
fixture.detectChanges(); // get the spy promise and wait for it to resolve
spy.calls.mostRecent().returnValue.then(() => {
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
done();
});
});
测试输入输出组件
带有导入和导出的组件通常出现在宿主组件的视图模板上,使用属性绑定来设置输入属性。使用事件绑定来监听输出属性触发的事件。
测试的目的是验证这样的绑定和期待的那样正常工作,测试程序应该设置导入值并监听导出事件。
<dashboard-hero *ngFor="let hero of heroes" class="col-1-4"
[hero]=hero (selected)="gotoDetail($event)" >
</dashboard-hero> @Component({
selector: 'dashboard-hero',
templateUrl: './dashboard-hero.component.html',
styleUrls: [ './dashboard-hero.component.css' ]
})
export class DashboardHeroComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
例如上面的代码,*ngFor循环中设置每个组件的hero input属性到迭代的值,并监听组件selected事件。
有下列几种测试方案:
把它当做独立的组件来测试
把它当做被替代组件使用的组件来测试
独立测试
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ],
})
.compileComponents(); // compile template and css
})); // synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(DashboardHeroComponent);
comp = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element // pretend that it was wired to something that supplied a hero
expectedHero = new Hero(42, 'Test Name');
comp.hero = expectedHero;
fixture.detectChanges(); // trigger initial data binding
});
使用compileComponents异步编译完成组件后,设置执行另一个同步的beforeEach
值得注意的是,expectedHero赋值给组件hero属性的方式,模拟了迭代器中通过属性绑定的赋值方式。
然后看测试程序
it('should display hero name', () => {
const expectedPipedName = expectedHero.name.toUpperCase();
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
});
验证英雄名字通过绑定被传递到模板了,实际页面如下
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>
第二个测试程序验证点击行为,点击英雄应该触发selected事件,可供宿主组件监听:
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero); heroEl.triggerEventHandler('click', null);
expect(selectedHero).toBe(expectedHero);
});
这个组件公开EventEmitter属性,测试程序像宿主组件那样来描述它
heroEl是个DebugElement,代表了英雄所在的<div>。测试程序用click事件名字来调用triggerEventHandler
调用.click()时,click事件绑定作出相应。
如果组件像期待的那样工作,click通知组件的selected属性就会发出hero对象,测试程序通过订阅
selected事件而检测到这个值,所以测试应该成功。
triggerEventHandler方法
Angular的DebugElement.triggerEventHandler可以用事件的名字触发任何数据绑定事件。
第二个参数是传递给事件处理器的事件对象。测试程序用null事件对象触发click事件
heroEl.triggerEventHandler('click', null);
测试程序假设运行事件的事件处理器-组件的click方法-不关心事件对象。
其他处理器将会更加严格。比如RouterLink指令期待事件对象,并且该对象具有button属性,
代表了已被按下的鼠标按钮。如果该事件对象不具备上面的条件,指令变会抛出错误。
点击按钮、链接、任意HTML元素是很常见的测试任务。
把click触发过程封装到辅助方法中可以简化这个任务,比如下面的click辅助方法:
/** Button events to pass to `DebugElement.triggerEventHandler` for RouterLink event handler */
export const ButtonClickEvents = {
left: { button: 0 },
right: { button: 2 }
}; /** Simulate element click. Defaults to mouse left-button click event. */
export function click(el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left): void {
if (el instanceof HTMLElement) {
el.click();
} else {
el.triggerEventHandler('click', eventObj);
}
}
第一个参数是用来点击的元素,如果你愿意,可以将自定义的事件对象传递给第二个参数。
默认的是鼠标左键事件对象,它被许多事件处理器接受,包括RouterLink指令。
下面是使用click辅助函数重新编写的上一个测试程序。
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero); click(heroEl); // triggerEventHandler helper
expect(selectedHero).toBe(expectedHero);
});
在测试宿主组件中测试组件
在前面的方法中,测试本身扮演了宿主组件的角色。
当正常数据绑定到宿主组件时,还会正常工作吗?
使用实际的宿主来测试是可行的,但是这么做似乎不合算。像下面这样使用测试宿主组件来模拟更加容易
//dashboard-hero.component.spec.ts @Component({
template: `
<dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>`
})
class TestHostComponent {
hero = new Hero(42, 'Test Name');
selectedHero: Hero; onSelected(hero: Hero) {
this.selectedHero = hero;
}
}
测试宿主组件不用理会Router、HeroService服务,甚至*ngFor循环。
测试宿主将组件的hero导入属性设置为它的模拟英雄,将组件的selected事件绑定到它的onSelected处理器,
使用selectedHero属性来记录发送来的英雄,然后测试检查这个属性来验证事件确实发送了正确的英雄。
配置使用测试宿主的测试程序与配置孤立测试相似。
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both
}).compileComponents();
})); beforeEach(() => {
// create TestHostComponent instead of DashboardHeroComponent
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero
fixture.detectChanges(); // trigger initial data binding
});
这个测试模块配置展示了两个非常重要的区别:
它同时声明了DashboardHerComponent和TestHostComponent
它创建了TestHostComponent,而非DashboardHeroComponent
createComponet返回fixture里有TestHostComponent实例,而非DashboardHeroComponet组件实例。
创建TestHostComponet有创建DashboardHero的副作用,因为后者出现在前者的模板中。
英雄元素的查询语句仍然可以在测试DOM中找到他,尽管元素树比以前更深。
it('should display hero name', () => {
const expectedPipedName = testHost.hero.name.toUpperCase();
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
}); it('should raise selected event when clicked', () => {
click(heroEl);
// selected hero should be the same data bound hero
expect(testHost.selectedHero).toBe(testHost.hero);
});
只有selected事件的测试不一样,确保被选择的DashboardHero英雄确实通过事件绑定被传递到宿主组件
测试带路由器的组件
测试实际的DashbaordComponent似乎令人生畏,因为注入了Router
constructor(
private router: Router,
private heroService: HeroService) {
}
同时还注入了HeroService,但是我们已经知道如何伪造它。Router的API非常复杂,并且它缠绕了其他服务和许多应用的先决条件。
幸运的是,DashbaordComponet没有使用Router做很多事情
gotoDetail(hero: Hero) {
let url = `/heroes/${hero.id}`;
this.router.navigateByUrl(url);
}
通常都是这样的,原则上,你测试的是组件,不是路由器,应该只关心在指定的条件下,
组件是否导航到正确的地址。用模拟类来替换路由器是一种简单的方案,下面代码应该可以:
class RouterStub {
navigateByUrl(url: string) { return url; }
}
现在我们来利用Router和HeroService的测试stub类来配置测试模块,并为接下来的测试创建实例
beforeEach( async(() => {
TestBed.configureTestingModule({
providers: [
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub }
]
})
.compileComponents().then(() => {
fixture = TestBed.createComponent(DashboardComponent);
comp = fixture.componentInstance;
});
下面的测试程序点击显示的英雄,并利用spy来确认Router.navigateByUrl被调用了而且传进的url是所期待的值。
it('should tell ROUTER to navigate when hero clicked',
inject([Router], (router: Router) => { // ... const spy = spyOn(router, 'navigateByUrl'); heroClick(); // trigger click on first inner <div class="hero"> // args passed to router.navigateByUrl()
const navArgs = spy.calls.first().args[0]; // expecting to navigate to id of the component's first hero
const id = comp.heroes[0].id;
expect(navArgs).toBe('/heroes/' + id,
'should nav to HeroDetail for first hero');
}));
inject函数
注意第二个it参数里面的inject函数。
it('should tell ROUTER to navigate when hero clicked',
inject([Router], (router: Router) => { // ...
}));
inject函数是Angular测试工具之一,注入服务到测试函数,以供修改、监视、操纵
inject函数有两个参数:
1.一列数组,包含了Angular依赖注入令牌
2.一个测试函数,参数与注入令牌数组里的每个项目严格的一一对应
使用TestBed注入器来注入
inject函数使用当前的TestBed注入器,并且只返回这个级别提供的服务,不会返回组件提供商提供的服务。
这个例子通过当前的TestBed注入器来注入Router,对这个测试程序员来说,这是没问题的。
因为Router是由应用的根注入器来提供的。
如果你需要组件自己的注入器提供的服务,调用
userService = fixture.debugElement.injector.get(UserService);
使用组件自己的注入器来获取实际注入到组件的服务。
inject函数关闭当前TestBed实例,使他无法再被配置。不能再调用任何TestBed配置方法,configureTestModule或者任何override方法,否则TestBed将抛出错误。
不要再调用inject以后再试图配置TestBed。
测试带有路由和路由参数的组件
点击英雄触发导航到hero/:id,其中id就是路由参数。
路由器将:id令牌的值推送到ActivateRoute.params可观察属性里,Angular注入ActivatedRoute到HeroDetailComponent中,然后组件提取id,这样他就可以通过HeroDetailService获取相应的英雄。
constructor(
private heroDetailService: HeroDetailService,
private route: ActivatedRoute,
private router: Router) {
}
HeroDetailComponent在它的ngOnInit方法中监听ActivatedRoute.params的变化
ngOnInit(): void {
// get hero when `id` param changes
this.route.paramMap.subscribe(p => this.getHero(p.has('id') && p.get('id')));
}
route.params之后的表达式链接了可观察操作符,从params中提取id然后链接forEach操作符来订阅id变化事件。每次id变化时,用户被导航到不同的英雄。
forEach将新的id值传递到组件的getHero方法,获取英雄并将它赋值到组件的hero属性。
如果id参数无效,pluck操作符就会失败,catch将失败当做创新英雄来处理。
通过操纵被注入到组件构造函数的ActivatedRoute服务,测试程序可以探索HeroDetail是如何对不同的id参数值作出相应的。
现在你已经知道如何模拟Router和数据服务,模拟ActivatedRoute遵循类似的模式,但是有个额外枝节:ActivatedRoute.params是可观察对象。
可观察对象的测试替身
detail.spec.ts依赖ActivatedRouteStub来为每个测试程序设置ActivatedRoute.params值。
是跨应用、可复用的测试辅助类,建议将这样的辅助类放在app目录下的testing的目录
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { convertToParamMap, ParamMap } from '@angular/router'; @Injectable()
export class ActivatedRouteStub { // ActivatedRoute.paramMap is Observable
private subject = new BehaviorSubject(convertToParamMap(this.testParamMap));
paramMap = this.subject.asObservable(); // Test parameters
private _testParamMap: ParamMap;
get testParamMap() { return this._testParamMap; }
set testParamMap(params: {}) {
this._testParamMap = convertToParamMap(params);
this.subject.next(this._testParamMap);
} // ActivatedRoute.snapshot.paramMap
get snapshot() {
return { paramMap: this.testParamMap };
}
}
这个stub类有下列值得注意的特征:
这个stub类只实现ActivatedRoute的两个功能:params和snapshot.params
BehaviorSubject驱使这个stub类的params可观察对象,并为每个params的订阅者返回同样的值,直到他接收到新值。
HeroDetail链接它的表达式到这个stub类的params可观察对象,该对象现在被测试者的控制之下。
设置testParams属性导致subject将指定的值推送进params,触发上面描述过的HeroDetail的params订阅,和导航的方式一样。
设置testParams属性同时更新这个stub类内部值,用于snapshot属性的返回
(snapshot是组件使用路由参数的另一种流行方法)
测试可观察对象的替身
describe('when navigate to existing hero', () => {
let expectedHero: Hero; beforeEach( async(() => {
expectedHero = firstHero;
activatedRoute.testParamMap = { id: expectedHero.id };
createComponent();
})); it('should display that hero\'s name', () => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
});
https://angular.cn/guide/testing#the-second-synchronous-beforeeach
Karma与TSLint的更多相关文章
- angularjs自动化测试系列之karma
angularjs自动化测试系列之karma karma test with jasmine 更好的利用工具是为了让生活更美好. 需要安装的东西: npm install karma -g mkdir ...
- 使用karma测试平时写的小demo(arguments为例)
有人说前端自动化测试非常困难,我觉得确实如此.在项目中,我个人也不放心写的测试,还是要手动测试.但是我们平时写demo学习时,完全可以使用自动化测试. 传统demo 1,新建一个html 2,写入js ...
- karma单元测试入门
学习angularjs,都会遇到karma单元测试,可是初学者面对复杂的测试配置往往不知从何入手,下面我们将抛开angularjs,单独使用两个js文件,完成一次测试入门. 0,karma原理
- 利用Angularjs测试引擎Karma进行自动化单元测试
Karma是Google用于angularjs框架单元测试的js引擎(javascript test runner ), angular1 和angular2项目源码的单元测试都是基于karma和ja ...
- karma与webpack结合
一.必备插件 1.babel:es6的语法支持 2.karma:测试框架 3.jasmine:断言框架 4.webpack:打包工具 5.karma-webpack:karma调用webpack打包接 ...
- karma的基础应用之与fis结合
一.介绍 1. karma是单元测试运行框架,可以集成jasmine断言库,也支持babel. 2.fis是百度前端团队开源推出的前端工程化管理工具. 二.karma的基础应用 1.karma的基础a ...
- Angular+Grunt+Bower+Karma+Protractor (Atom)
1. 配置bower 1.安装bower npm install -g bower 2.创建.bowerrc文件 { "directory": "src/bower&qu ...
- Ubuntu上安装Karma失败对策
在Ubuntu上安装Karma遇到超时 timeout 错误.Google了一下,国外的码农给了一个快捷的解决方案,实测可行,贴在这里: sudo apt-get install npm nodejs ...
- Karma+Jasmine实现自动化单元测试
1.Karma介绍 Karma是Testacular的新名字,在2012年google开源了Testacular,2013年Testacular改名为Karma.Karma是一个让人感到非常神秘的名字 ...
随机推荐
- freemarker模板加载TemplateLoader常见方式
使用过freemarker的肯定其见过如下情况: java.io.FileNotFoundException: Template xxx.ftl not found. 模板找不到.可能你会认为我明明指 ...
- Spring@PostConstruct注解和构造方法的调用顺序
先看下@PostConstruct的注解 * The PostConstruct annotation is used on a method that needs to be executed * ...
- struts2的动态方法调用(DMI)和通配符映射
动态方法调用 1.Struts2默认关闭DMI功能,需要使用需要手动打开,配置常量 struts.enable.DynamicMethodInvocation = true 2.使用“!”方法,即 ...
- android学习1:清晰详细android环境搭建,超简单
废话少说,今天是Android学习的开篇的博客,接下来将把自己学习android的各种问题和经历总结一下,其实之前已经自己学过半年了,但是因为开始时刚学的移动端开发还没有概念,当时总结工作又做的不好, ...
- jQuery添加标签实例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Tarjan算法初探(3):求割点与桥以及双连通分量
接上一节Tarjan算法初探(2):缩点 在此首先提出几个概念: 割点集合:一个无向连通图G 若删除它的一个点集 以及点集中所有点相连的边(任意一端在点集中)后 G中有点之间不再连通则称这个点集是它的 ...
- 关于DP
关于DP 似乎摸到了门槛呢,学着学着Dijkstra突然有了感觉. 我们遍历的时候会遍历整张图的每个点每条边,然后与已知的对比大小,如果比现在方案好,就放入数组 那么,DP岂不是同样的思想? 在背包问 ...
- dfs板子题-Hdu1283Vegetables
题目描述毕业后,Vegetable在一家建筑公司找到了工作.他所在的城市将要进行整修,要求把所有空地修成公园. 市区是一个N*M的矩形,Vegetable拿到了该市的地图,现在判断共要修几处公园? 注 ...
- solve the promble of VMware Workstation Ubuntu18.04 ethernet interface losting
$ ifconfig -aens33: flags=4098<BROADCAST,MULTICAST> mtu 1500 ether 00:**:**:**:**:** txqu ...
- (转载)SendKeys.Send()的使用
SendKeys.Send() 使用SendKeys将键击和组合键击发送到活动应用程序.此类无法实例化.若要发送一个键击给某个类并立即继续程序流,请使用Send.若要等待键击启动的任何进程,请使用Se ...