Angular单元测试系列??Component、Directive、Pipe 以及Service
作者:cipchk 发布时间:[ 2017/6/15 10:55:18 ] 推荐标签:软件测试 单元测试 Angular
4、佳实践
至此,我总感觉有些不对。
那便是 TradeService ,我们太过于依赖它了。假如 TradeService 的依赖变动了,那是不是还得再修改测试组件依赖,而且 TradeService 可能会在很我多测试文件中出现,所以这样做很蛋疼。
怎么办?
回想,Angular强大的功能DI,它解决了各种依赖关系的复杂问题。但是,从测试角度出发,如果说组件与组件之间的依赖关系也在单元测试中出现,这样的事情很让人受不了。
正如 TradeService 内部还依赖 UserService ,以至于,还需要注入 UserService ,这都算什么事嘛。
当然,好的组件测试代码应该是纯洁的、干净的。
Stub
trade-list 组件依赖 TradeService ,而 TradeService 又依赖 UserService ,那么何不我们直接人为捏造一个 TradeService 呢?然后让这种依赖见鬼去。
Angular强大DI系统,可以简单帮助我们解决这个问题。以下使用 trade-list 组件为例:
const tradeServiceStub = {
query(): Observable<any[]> {
return Observable.of(tradeData);
}
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TradeListComponent, TradePipe],
providers: [
{ provide: TradeService, useValue: tradeServiceStub }
]
}).compileComponents();
// 等同上面
}));
@NgModule 测试模块的写法和上面大概一样,只不过这里我们捏造了一个 tradeServiceStub ,并且在注入 TradeService 时采用捏造的数据而已,这么简单……
因此,这里看不到 HttpModule 、 UserService 了,干净了!
5、小结
如果按举一反三来讲的话,上面大概可以完成所有包括:Directive、Pipe、Service这些测试编码了。
故,为了完整度,后续可能会出现一些和Component相同的内容。
三、Directive
1、示例
一个点击次点指令。
@Directive({ selector: '[log]' })
export class LogDirective {
tick: number = 0;
@Output() change = new EventEmitter();
@HostListener('click', [ '$event' ])
click(event: any) {
++this.tick;
this.change.emit(this.tick);
}
}
2、测试模块 @NgModule
// 测试容器组件
@Component({
template: `<div log (change)="change($event)"></div>`
})
class TestComponent {
@Output() changeNotify = new EventEmitter();
change(value) {
this.changeNotify.emit(value);
}
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, LogDirective]
});
fixture = TestBed.createComponent(TestComponent);
context = fixture.componentInstance;
el = fixture.nativeElement;
let directives = fixture.debugElement.queryAll(By.directive(LogDirective));
directive = directives.map((d: DebugElement) => d.injector.get(LogDirective) as LogDirective)[0];
});
这里没有涉及外部模板或样式,所以无须采用 beforeEach 异步。
但和上面又略有不同的是,这里利用 By.directive 来查找测试容器组件的 LogDirective 指令。
By
By 是Angular提供的一个快速查找工具,允许传递一个 Type 类型的指令对象,这样给我们很多便利。它还包括: css 、 all 。
3、测试用例
A、确保初始化
只需要确保 beforeEach 获取的指令对象存在,都可以视为正确初始化,当然你也可以做更多。
it('should be defined on the test component', () => {
expect(directive).not.toBeUndefined();
});
B、HostListener测试
[log] 会监听父宿主元素的 click 事件,并且触发时会通知 change 自定义事件。因此,需要给测试容器组件添加一个 (change) 事件,而我们要测试的是,当我们触发测试容器组件中的按钮后,是否会触发该事件。
it('should increment tick (fakeAsync)', fakeAsync(() => {
context.changeNotify.subscribe(val => {
expect(val).toBe(1);
});
el.click();
tick();
}));
这里采用 fakeAsync 异步测试方法,因为 changeNotify 的执行是需要事件触发以后才会接收到的。
首先,订阅测试容器组件的 changeNotify ,并以是否接收到数值 1 来表示结果(因为功能本身是点击一个+1,开始默认为:0)。
其次,触发DOM元素的 click() 事件。
而 tick() 会中断执行,直到订阅结果返回。
四、Pipe
1、示例
一个根据交易状态返回中文文本的Pipe。
@Pipe({ name: 'trade' })
export class TradePipe implements PipeTransform {
transform(value: any, ...args: any[]) {
switch (value) {
case 'new':
return '新订单';
case 'wait_pay':
return '待支付';
case 'cancel':
return `<a title="${args && args.length > 0 ? args[0] : ''}">已取消</a>`;
default:
throw new Error(`无效状态码${value}`);
}
}
}
2、测试用例
A、确保初始化
Pipe相当于一个类,而对类的测试简单的,只需要主动创建一个实例行了。
当然,你也无法使用 async 、 fakeAsync 之类的Angular工具集提供便利了。
let pipe = new TradePipe();
it('should be defined', () => {
expect(pipe).not.toBeUndefined();
});
B、new 状态码
it(`should be return '新订单' with 'new' string`, () => {
expect(pipe.transform('new')).toEqual('新订单');
});
C、测试DOM渲染
以上测试只能够测试Pipe是否能运行,而无法保证DOM渲染功能是否可用。因此,需要构建一个测试容器组件。
@Component({ template: `<h1>{{ value | trade }}</h1>` })
class TestComponent {
value: string = 'new';
}
let fixture: ComponentFixture<TestComponent>,
context: TestComponent,
el: HTMLElement,
de: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, TradePipe]
});
fixture = TestBed.createComponent(TestComponent);
context = fixture.componentInstance;
el = fixture.nativeElement;
de = fixture.debugElement;
});
与之前看到的 NgModule 并无什么不一样。
测试用例
检验 h1 DOM标签的内容是不是如我们期望的即可。
it('should display `待支付`', () => {
context.value = 'wait_pay';
fixture.detectChanges();
expect(el.querySelector('h1').textContent).toBe('待支付');
});
五、Service
1、示例
交易类:
@Injectable()
export class TradeService {
constructor(private http: Http, private userSrv: UserService) { }
query(): Observable<any[]> {
return this.http
.get('./assets/trade-list.json?token' + this.userSrv.token)
.map(response => response.json());
}
private getTrade() {
return {
"id": 10000,
"user_id": 1,
"user_name": "asdf",
"sku_id": 10000,
"title": "商品名称"
}
}
get(tid: number): Promise<any> {
return new Promise(resolve => {
setTimeout(() => {
resolve(this.getTrade());
}, 500);
})
}
}
用户类:
@Injectable()
export class UserService {
token: string = 'wx';
get() {
return {
"id": 1,
"name": "asdf"
};
}
type() {
return ['普通会员', 'VIP会员'];
}
}
2、测试用例
A、确保初始化
当Service无任何依赖时,我们可以像 Pipe 一样,直接构建一个实例对象即可。
当然,你也无法使用 async 、 fakeAsync 之类的Angular工具集提供便利了。
let srv: UserService = new UserService();
it(`#token should return 'wx'`, () => {
expect(srv.token).toBe('wx');
});
当然,绝大部分情况下不如所愿,因为真实的业务总是依赖于 Http 模块,因此,我们还需要 Angular工具集的支持,先构建一个 NgModule 。
let srv: TradeService;
beforeEach(() => TestBed.configureTestingModule({
imports: [HttpModule],
providers: [TradeService, UserService]
}));
beforeEach(inject([TradeService], s => { srv = s; }));
当然,它比我们上面任何一个示例简单得多了。
it('#query should return trade array', (done: DoneFn) => {
srv.query().subscribe(res => {
expect(Array.isArray(res)).toBe(true);
expect(res.length).toBe(2);
done();
});
});
B、为什么无法使用 async 、 fakeAsync
虽然Service基本上都是异步方法,但是这里并没有使用任何 async 、 fakeAsync ,哪怕我们的异步方法是 Observable 或 Promise。
细心的话,前面我提到两次。
当然,你也无法使用 async 、 fakeAsync 之类的Angular工具集提供便利了。
这是其中一方面。
而另一方面本示例也采用 Angular 工具集创建了 NgModule ,可为什么以下测试无法通过呢?
it('#get should return trade (fakeAsync)', fakeAsync((done: DoneFn) => {
srv.get(1).then(res => {
expect(res).not.toBeNull();
expect(res.id).toBe(10000);
});
tick();
}));
这是因为 tick() 本身只能等待诸如DOM事件、定时器、Observable以及Promise之类的,但是对于我们示例中 get() 是使用 setTimeout 来模拟一次请求要 500ms 呀,这对于 tick() 而言并不知道需要等待多长时间的呀。
所以,这里需要改成:
tick(600);
让等待的时长比我们 seTimeout 略长一点行啦。
六、结论
Angular单元测试其实非常简单,很多代码都是重复的。当然了,这里头的概念也没有很多,总归只是以 TestBed 为入口、以DI系统为简化依赖、以 spy 监听事件,仅此而已。
本节一共有24个用例,所有的你都可以在 plnkr 中找得到(注:由于 templateUrl 在plnkr中测试代码会失败,所以plnkr中依然以 template 形式)。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11