shark-test
为 shark 项目提供搭建测试环境的工具。
目录
一、安装
$ npm install --save-dev shark-test
二、安装测试相关的依赖
$ npm install --save classlist
$ npm install --save @types/jasmine
三、添加执行测试指令
在package.json文件中,添加测试指令
{
"scripts": {
"test": "./node_modules/shark-test/dist/src/index.js test"
},
}
四、初始化配置文件
执行以下初始化命令,自动创建四个测试相关的配置文件
或者
五、测试工具配置文件说明
在项目根目录下创建配置文件shark-test-conf.json,所有配置项都设了默认值
{
//项目的源码路径--所有配置基于该路径
"basePath": "src/main/webapp",
// 测试入口文件(.spec.ts)
"main": "test.ts",
// 测试环境的polyfills
"polyfills": "polyfills",
// karma的配置文件
"configFile": "karma.conf.js",
// 项目组件目录
"componentPath": "app",
// 项目资源
"assets": ["assets", "favicon.ico"],
// 项目模板
"indexTemplate": "index.ejs"
}
测试入口文件
在配置文件中设置的basePath
路径下添加用于测试入口文件test.ts
import "zone.js/dist/long-stack-trace-zone";
import "zone.js/dist/proxy";
import "zone.js/dist/sync-test";
import "zone.js/dist/jasmine-patch";
import "zone.js/dist/async-test";
import "zone.js/dist/fake-async-test";
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
const context = require.context('./', true, /\.spec\.ts$/);
context.keys().map(context);
测试环境的polyfills.ts
在配置文件中设置的basePath
路径下添加用于测试环境的polyfills.ts文件
import 'core-js/es6/symbol';
import 'core-js/es6/object';
import 'core-js/es6/function';
import 'core-js/es6/parse-int';
import 'core-js/es6/parse-float';
import 'core-js/es6/number';
import 'core-js/es6/math';
import 'core-js/es6/string';
import 'core-js/es6/date';
import 'core-js/es6/array';
import 'core-js/es6/regexp';
import 'core-js/es6/map';
import 'core-js/es6/weak-map';
import 'core-js/es6/set';
import 'classlist.js';
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
karma配置文件
在项目根目录下创建karma配置文件karma.conf.js
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', 'shark'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('shark-test/dist/src/plugin/karma')
],
client: {
clearContext: false
},
coverageIstanbulReporter: {
reports: ['html', 'lcovonly'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9888,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};
六、使用
运行测试代码
或者
或者
七、FAQ
八、angular测试手册
各类测试说明:
一、Component(带输入输出的组件、带路由的组件、带依赖的组件、嵌套组件)
- 组件类测试
像测试服务类一样测试组件类,单独测试组件类本身而不必涉及DOM。
例子1:下面是一个列表组件ProduceScheduleComponent(带路由的组件、带依赖的组件、嵌套组件)
import { Component, ViewChild } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { ProduceScheduleService } from './produce-schedule.service';
@Component({
styleUrls: ['./produce-schedule.component.scss'],
templateUrl: './produce-schedule.component.html'
})
export class ProduceScheduleComponent {
purchaseOrderList: Array<any>;
pagination: any;
constructor(
private router: Router,
private route: ActivatedRoute,
private produceScheduleService: ProduceScheduleService
) {
}
ngOnInit() {
let params={
page:1,
size:20
}
this.getPurchaseList(params)
}
getPurchaseList(params) {
this.produceScheduleService.getProceedingList(params).then(data => {
this.purchaseOrderList = data.result
this.pagination = data.pagination
}).catch(err => { })
}
}
组件对应的测试代码
import { TestBed, async } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { SharkValid, SharkValidForm, Common } from "@shark/shark-angularX";
import { RouterTestingModule } from '@angular/router/testing';
import { ProduceScheduleComponent } from './produce-schedule.component';
import { ProduceScheduleService } from './produce-schedule.service';
describe('#ProduceScheduleComponent', () => {
let produceScheduleService;
let fixture;
let component;
let getProceedingListSpy;
let selectDom;
let data = {
code: 200,
data: {
pagination: {
page: 1,
size: 10,
total: 354,
totalPage: 36,
offset: 0
},
result: [{}]
}
};
beforeEach(() => {
const produceScheduleService = jasmine.createSpyObj('ProduceScheduleService', ['getProceedingList']);
getProceedingListSpy = produceScheduleService.getProceedingList.and.returnValue(new Promise<any>((resolve, reject) => { resolve(data) }));
TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [ProduceScheduleComponent, SharkValidForm, SharkValid],
providers: [{ provide: ProduceScheduleService, useValue: produceScheduleService }, Common],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(ProduceScheduleComponent);
component = fixture.componentInstance;
});
it('should create the ProduceScheduleComponent', async(() => {
expect(component).toBeTruthy();
}));
it('should call getProceedingList after component initialized', () => {
fixture.detectChanges();
expect(getProceedingListSpy.calls.any()).toBe(true, 'getProceedingList called');
});
});
例子2:下面是一个列表组件ProduceScheduleComponent(带输入的组件)
调用组件代码
<schedule-item [purchaseItem]="item"></schedule-item>
ScheduleItemComponent组件的测试代码
import { TestBed, async } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { SharkToastrService, SharkModalService } from "@shark/shark-angularX";
import { RouterTestingModule } from '@angular/router/testing';
import { ScheduleItemComponent } from './schedule-item.component';
import { PurchaseStatePipe } from '../purchase-state.pipe'
import { SharedModule } from '../../../shared/shared.module'
describe('#ScheduleItemComponent', () => {
let fixture;
let component;
let purchaseOrderEl;
let purchaseItem = {
purchaseOrder: "1375870000011112-20171024-32",
allowModify: true,
isVersion1_0: 0,
purchaseOrderType: 2,
purchaseOrderSaleModel: 1,
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, SharedModule],
declarations: [ScheduleItemComponent, PurchaseStatePipe],
providers: [SharkToastrService, SharkModalService],
schemas: [NO_ERRORS_SCHEMA]
});
fixture = TestBed.createComponent(ScheduleItemComponent);
component = fixture.componentInstance;
component.purchaseItem = purchaseItem;
fixture.detectChanges();
});
it('should display same purchaseOrder as purchaseItem.purchaseOrder ', () => {
purchaseOrderEl = fixture.nativeElement.querySelectorAll('b')[0];
const purchaseOrder = purchaseItem.purchaseOrder;
expect(purchaseOrderEl.textContent).toContain(purchaseOrder);
});
});
- 组件Dom测试
测试组件是否能正常渲染出来、响应用户的输入和查询或与它的父组件和子组件相集成
例子:略
二、Directive
Directive作为宿主元素的属性来被使用的, 需要创建宿主元素来对指令的行为进行测试
例子:ShowAndHideListDirective的功能是控制table行数展示,并有展开、收起功能
import { Directive, ElementRef, HostListener, Input } from '@angular/core'
@Directive({
selector: '[showAndHideList]'
})
export class ShowAndHideListDirective {
private el: HTMLElement;
@Input('showAndHideList') initNum: number;
constructor(el: ElementRef) {
this.el = el.nativeElement;
this.el.style.backgroundColor = '#f4f4f4';
}
@HostListener('click') onClick() {
var isHide = false;
var groups = this.el.parentElement.parentElement.getElementsByTagName('tr');
for (var i = this.initNum; i < groups.length; i++) {
var item = groups[i];
if (item.id == 'showAndHide') {
break;
}
if (item.classList.contains('hide')) {
item.classList.remove('hide');
isHide = true;
} else {
item.classList.add('hide');
isHide = false;
}
}
if (isHide) {
this.el.innerHTML = "收起商品<i class = 'icon-up'></i>"
} else {
this.el.innerHTML = "全部商品<i class = 'icon-down'></i>"
}
}
}
ShowAndHideListDirective指令的测试代码
import { Component, OnInit } from '@angular/core'
import { TestBed } from '@angular/core/testing';
import { ShowAndHideListDirective } from './showAndHideList.directive'
@Component({
template: `
<div class="table-wrap margin-b-4x">
<table class="table table-full text-center">
<thead>
<tr>
<th>严选SKU ID</th>
<th>商品名称</th>
<th>规格</th>
</tr>
</thead>
<tbody class="text-center">
<tr *ngFor="let item of skuItems;index as i " [ngClass]="{'hide':i > initNum-1}">
<td>{{item.skuId}}</td>
<td>{{item.itemName}}</td>
<td>{{item.specValueDesc}}</td>
</tr>
<tr [ngClass]="{'hide': initNum >= skuItems.length}" id="showAndHide">
<td [showAndHideList]="initNum" [attr.colspan]="3" class="text-center text-gold text-cursor-pointer ">全部商品
<i class="icon-down"></i>
</td>
</tr>
</tbody>
</table>
</div>`,
styleUrls: ['./test.component.scss']
})
class TestComponent {
initNum: any = 3;
skuItems: any = [{
itemId: 10843001,
itemName: "简约收纳盒",
specValueDesc: "尺寸:大",
skuId: 10924002
}, {
itemId: 10843001,
itemName: "简约收纳盒",
specValueDesc: "尺寸:中",
skuId: 10924003
}, {
itemId: 10843001,
itemName: "简约收纳盒",
specValueDesc: "尺寸:小",
skuId: 10924004
},
{
itemId: 10843001,
itemName: "女士水桶包",
specValueDesc: "颜色:红",
skuId: 10924005
},
{
itemId: 10843001,
itemName: "女士水桶包",
specValueDesc: "颜色:黑",
skuId: 10924006
},
{
itemId: 10843001,
itemName: "女士水桶包",
specValueDesc: "颜色:白",
skuId: 10924007
},
{
itemId: 10843001,
itemName: "男士T恤",
specValueDesc: "尺寸:M",
skuId: 10924008
},
{
itemId: 10843001,
itemName: "男士T恤",
specValueDesc: "尺寸:L",
skuId: 10924009
}
];
}
describe('ShowAndHideListDirective', () => {
let fixture;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ShowAndHideListDirective, TestComponent]
}).createComponent(TestComponent);
});
it('should have 10 <tr>', () => {
fixture.detectChanges();
const tr: any = fixture.nativeElement.querySelectorAll('tr');
expect(tr.length).toBe(10);
});
it('should have 5 <tr> with hide', () => {
fixture.detectChanges();
const tr: any = fixture.nativeElement.querySelectorAll('.hide');
expect(tr.length).toBe(5);
});
it('should have 0 <tr> with hide', () => {
const display = fixture.nativeElement.querySelector('#showAndHide');
display.click();
const tr: any = fixture.nativeElement.querySelectorAll('.hide');
expect(tr.length).toBe(0);
});
});
三、Service(非http服务)
service的测试 非http服务基本上不需要依赖Angular的测试工具集.按普通类对待就好,我们以下面的例子说明
eg .目录结构如下
|-- demo
|-- master.service.js
|-- master.service.spec.js
|-- value.service.js
|-- value.service.spec.ts
valueService 是一个简单的Service。包含有若干方法
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
@Injectable()
export class ValueService {
constructor() { }
getValue() {
return { data: 123 }
}
getValue1() {
return Observable.of({ data: 456 })
}
getValue2() {
return new Promise((resolve, reject) => {
resolve({ data: 789 })
})
}
}
对ValueService的测试如下:你会发现,和我们平时写代码是一样的,只不过多了一个断言判断
import { ValueService } from './value.service'
describe('MasterService', () => {
let service: ValueService;
beforeEach(() => {
service = new ValueService();
})
it('#getValue should return real value', () => {
expect(service.getValue()).toEqual({ data: 123 })
})
it('#getValue1 should return value from observable', async () => {
service.getValue1().subscribe(value => {
expect(value).toEqual({ data: 456 })
})
})
it('#getValue2 should return value from promise', async () => {
service.getValue2().then(value => {
expect(value).toEqual({ data: 789 })
})
})
})
MasterService 依赖了ValueService
import { Injectable } from '@angular/core';
import { ValueService } from './value.service';
@Injectable()
export class MasterService {
constructor(private valueService: ValueService) {
}
getValue() {
return this.valueService.getValue();
}
}
对于有依赖的服务。有很多种测试方案:
- 如果依赖的服务相对简单,而且可靠,这时候可以直接引入真实依赖。做法和产品代码环境一样:
import { ValueService } from './value.service'
import { MasterService } from './demo'
describe('MasterService real', () => {
let service: ValueService;
let masterServie: MasterService;
beforeEach(() => {
service = new ValueService();
masterServie = new MasterService(service);
})
it('#getValue should return real value', () => {
expect(masterServie.getValue()).toEqual({ data: 123 })
})
})
- Spy:利用spyObj来替换真实的依赖,并借助testBed创建类并注入服务
import { ValueService } from './value.service'
import { MasterService } from './master.service'
import { TestBed } from '@angular/core/testing';
describe('', () => {
let masterService: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('ValueService', ['getValue']);
TestBed.configureTestingModule({
providers: [
MasterService,
{ provide: ValueService, useValue: spy }
]
});
masterService = TestBed.get(MasterService);
valueServiceSpy = TestBed.get(ValueService);
});
it('#getValue should return stubbed value from a spy', () => {
const stubValue = { data: 123 };
valueServiceSpy.getValue.and.returnValue(stubValue);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
})
如果我们要测的服务含有http请求,一般是采用mock的方式,考虑到我们项目中的的实际情况,这里的测试验证点在于:
1、是否发出了请求(代码是否执行)
2、是否发出了预期的请求(请求路径是否正确、请求方法是否正确)
3、是否发出了非预期的请求(有没有胡乱发送请求)
4、对返回code的处理是否符合预期(code = 200正确预期是否成立 ,code = 400,错误的预期是否成立)
eg.
以大制造家中的supplier-info.service.ts为例
import { Injectable } from '@angular/core'
import { Ajax } from '@shark/shark-angularX'
import { Observable } from 'rxjs'
@Injectable()
export class SupplierInfo {
private getSupplierInfoUrl = '/login/getSupplierInfo.json'
infoData: any = void 0
constructor(
private ajax: Ajax
) {
}
getSupplierInfo(obj: Object = {}) {
if (this.infoData) {
return Observable.of(this.infoData);
}
else {
return Observable.fromPromise<any>(
this.ajax.get(this.getSupplierInfoUrl, obj).then(data => {
this.infoData = data
return this.infoData
})
)
}
}
}
其测试用例的写法为:具体见注释
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Common, Ajax, Cookie } from '@shark/shark-angularX'
import { TestBed, tick } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { SupplierInfo } from './supplier-info.service';
describe('#SupplierInfo', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let supplierInfo: SupplierInfo;
let ajax: Ajax;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [Ajax, Common, Cookie, SupplierInfo],
schemas: [NO_ERRORS_SCHEMA]
});
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
ajax = TestBed.get(Ajax);
supplierInfo = TestBed.get(SupplierInfo);
this.expectedSupplierInfoSuccess = {
code: 200,
data: {
}
};
this.expectedSupplierInfoFail = {
code: 400,
errorCode: 'xxxx'
}
});
afterEach(() => {
httpTestingController.verify();
});
it('SupplierInfo service should be defined',
() => {
expect(supplierInfo).toBeDefined;
}
)
it('infoData should be undefinded before the first call for getSupplierInfo',
() => {
expect(supplierInfo.infoData).toBeUndefined;
}
)
it('infoData should be the supplierInfo after the first call for getSupplierInfo',
() => {
supplierInfo.getSupplierInfo();
const req1 = httpTestingController.expectOne('/login/getSupplierInfo.json');
req1.flush(this.expectedSupplierInfoSuccess);
supplierInfo.getSupplierInfo();
const req2 = httpTestingController.expectOne('/login/getSupplierInfo.json');
req2.flush(this.expectedSupplierInfoSuccess);
expect(supplierInfo.infoData).toBeDefined;
}
)
it('getSupplierInfo should return value from observable when the response is success',
() => {
supplierInfo.getSupplierInfo().subscribe(value => {
expect(value).toEqual(this.expectedSupplierInfoSuccess, 'should return expected expectedSupplierInfoSuccess'),
data => fail('should have successed with the data')
});
const req = httpTestingController.expectOne('/login/getSupplierInfo.json');
expect(req.request.method).toEqual('GET', 'the request method shoud be GET');
req.flush(this.expectedSupplierInfoSuccess);
});
it('getSupplierInfo should return value from observable when the response is fail',
() => {
supplierInfo.getSupplierInfo().subscribe(
data => fail('should have failed with the errorCode'),
value => {
expect(value).toEqual(this.expectedSupplierInfoFail, 'should return expected expectedSupplierInfoFail')
});
const req = httpTestingController.expectOne('/login/getSupplierInfo.json');
expect(req.request.method).toEqual('GET', 'the request method shoud be GET');
req.flush(this.expectedSupplierInfoFail);
});
});
pipe 除了 @Pipe元数据和一个接口基本上不依赖angular,所以它的测试很简单
eg.
以一个大制造家中的OrderBySkuId管道为例:
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'orderBySkuId' })
export class OrderBySkuId implements PipeTransform {
constructor(
) {
}
transform(list = []): any {
if (list && list.length > 0) {
return list.sort((a, b) => a.skuId - b.skuId);
} else {
return []
}
}
}
测试代码只需对其transform调用并断言即可:
import { OrderBySkuId } from './order-by-skuid.pipe';
import { each } from 'fpb';
describe('OrderBySkuIdPipe', () => {
const orderBySkuIdPipe = new OrderBySkuId();
const skuList = [{
skuId: 3
}, {
skuId: 4
}, {
skuId: 1
}, {
skuId: 2
}];
const expectedData = [{
skuId: 1
}, {
skuId: 2
}, {
skuId: 3
}, {
skuId: 4
}];
const expectedToEqualEmptyArr = each((v) => {
expect(orderBySkuIdPipe.transform(v)).toEqual([]);
})
it('transforms null、 undefiend、""、[] to []', () => {
expectedToEqualEmptyArr([null, undefined, '', []])
});
it('transforms list orderBy it\'s skuid property', () => {
expect(orderBySkuIdPipe.transform(skuList)).toEqual(expectedData);
});
})
- 改写组件provider
- Dom测试封装(见angular测试指导 page类)
- spy的使用
- jasmine-marbles的使用