最近在给组件补充测试,顺便整理一下常用的测试方法 spyOn、spyOnProperty、createSpy、createSpyObj等的关键知识点。
spyOn
- Jasmine 具有称为 spies 的双重测试功能。 spy可以存任何函数并跟踪对它的调用和所有参数。 spy仅存在于定义它的 describe 或 it 块中,并且在每个spec之后删除。
spyOn(obj: Object, methodName: string) → {Spy}
参数1:要安装spy的对象。参数2:要替换为 Spy 的方法的名称。返回值是一个Spy。- 以下示例包含toHaveBeenCalled、toHaveBeenCalledTimes、toHaveBeenCalledWith的使用。
// The example comes from Jasmine official
describe("A spy", function() {
var foo, bar = null;
beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
}
};
spyOn(foo, 'setBar'); // const fooSpy = spyOn(foo, 'setBar');
foo.setBar(123);
foo.setBar(456, 'another param');
});
it("tracks that the spy was called", function() {
expect(foo.setBar).toHaveBeenCalled(); // expect(fooSpy).toHaveBeenCalled();
});
it("tracks that the spy was called x times", function() {
expect(foo.setBar).toHaveBeenCalledTimes(2);
});
it("tracks all the arguments of its calls", function() {
expect(foo.setBar).toHaveBeenCalledWith(123);
expect(foo.setBar).toHaveBeenCalledWith(456, 'another param');
});
it("stops all execution on a function", function() {
expect(bar).toBeNull();
});
it("tracks if it was called at all", function() {
foo.setBar();
expect(foo.setBar.calls.any()).toEqual(true);
});
});
实际应用:
@Component({
template: `
<my-date-picker [ngModel]="value" (ngModelChange)="thyOnChange($event)"></my-date-picker>
`
})
class MyTestComponent {
value: Date | number;
thyOnChange(): void {}
}
describe('test date picker component event via spy on', () => {
let fixture: ComponentFixture<MyTestComponent>;
let fixtureInstance: MyTestComponent;
let debugElement: DebugElement;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule, MyDatePickerModule],
declarations: [MyTestComponent]
});
TestBed.compileComponents();
fixture = TestBed.createComponent(MyTestComponent);
fixtureInstance = fixture.componentInstance;
});
it('should support thyOnChange', fakeAsync(() => {
const initialDate = 1587629556;
fixtureInstance.value = initialDate;
const thyOnChange = spyOn(fixtureInstance, 'thyOnChange');
fixture.detectChanges();
dispatchMouseEvent(getPickerTriggerWrapper(), 'click'); // getPickerTriggerWrapper() 通过querySelector(selector)找到要点击的元素
fixture.detectChanges();
tick(500);
fixture.detectChanges();
expect(thyOnChange).toHaveBeenCalled(); // expect(fixtureInstance.thyOnChange).toHaveBeenCalled();
expect(thyOnChange).toHaveBeenCalledWith(initialDate);
}));
});
spyOnProperty
- 在一个[使用 Object.defineProperty 安装的]属性上安装spy。在Jasmine中,你可以用属性spy做任何[你可以用函数spy做的]事情。你可能需要使用不同的语法。
spyOnProperty(obj: Object, propertyName: string, accessType?: string) → {Spy}
参数1:安装spy的对象。参数2:属性的名称。参数3:属性的访问类型 (get|set),默认值是get。
// 使用spyOnProperty创建getter和setter spy。
it("allows you to create spies for either type", function() {
// 给someObject对象绑定myValue属性,访问形式是get
spyOnProperty(someObject, "myValue", "get").and.returnValue(30);
// 给someObject对象绑定myValue属性,访问形式是set
spyOnProperty(someObject, "myValue", "set").and.callThrough();
});
// 为了解决在不调用属性的getter方法的情况下无法引用属性这个问题,我们可以保存对spy的引用以供后面使用。
beforeEach(function() {
this.propertySpy = spyOnProperty(someObject, "myValue", "get").and.returnValue(1);
});
it("lets you change the spy strategy later", function() {
// 修改someObject.myValue的返回值
this.propertySpy.and.returnValue(3);
expect(someObject.myValue).toEqual(3);
});
// 如果保存对spy的引用很不方便,我们可以使用【Object.getOwnPropertyDescriptor】从测试中的任何位置访问它。
beforeEach(function() {
spyOnProperty(someObject, "myValue", "get").and.returnValue(1);
});
it("lets you change the spy strategy later", function() {
Object.getOwnPropertyDescriptor(someObject, "myValue").get.and.returnValue(3);
expect(someObject.myValue).toEqual(3);
});
// 你可以通过将数组或属性散列作为第三个参数传递给 createSpyObj 来快速创建具有多个属性的spy对象。 在这种情况下,你将没有对创建的spy的引用,因此如果以后需要更改它们的spy策略,必须使用 Object.getOwnPropertyDescriptor 方法。
it("creates a spy object with properties", function() {
// 创建一个具有多个属性的spy对象。
let obj = createSpyObj("myObject", {}, { x: 3, y: 4 });
expect(obj.x).toEqual(3);
Object.getOwnPropertyDescriptor(obj, "x").get.and.returnValue(7);
expect(obj.x).toEqual(7);
});
实际应用:
/**
* Nav导航组件的自适应功能:视口大小变化之后,调整tab的展示个数,展示不下的隐藏到“更多”中。
* 最后一个能展示得下的tab,它的offsetLeft+offsetWidth<nav组件的offsetLeft+offsetWidth。
*
* The example code comes from an open source component library called ngx-tethys.
*
* 功能实现的代码片段如下:
*/
// nav组件中nav的wrapperOffset
this.wrapperOffset = {
height: this.elementRef.nativeElement.offsetHeight || 0,
width: this.elementRef.nativeElement.offsetWidth || 0,
left: this.elementRef.nativeElement.offsetLeft || 0,
top: this.elementRef.nativeElement.offsetTop || 0
};
// 从右往左遍历tabs,判断哪个才是最后一个能展示得下的tab
const len = tabs.length;
let endIndex = len;
for (let i = len - 1; i >= 0; i -= 1) {
if (tabs[i].offset.left + tabs[i].offset.width < this.wrapperOffset.width + this.wrapperOffset.left) {
endIndex = i;
break;
}
}
// 测试 【Focus point: spyOnProperty】
function spyLinksAndNavOffset(links: ThyNavLinkDirective[], nav: ThyNavComponent) {
// 设置测试数据:每个tab(link)的宽高和左偏移量、上偏移量
(links || []).forEach((link, index) => {
link.offset = {
width: 30,
height: 30,
left: 30 * index,
top: 30 * index
};
});
// 设置测试数据:nav组件的宽高都是70,左偏移量和上偏移量是0。
// 对于ThyNavComponent组件,this.elementRef.nativeElement.offsetWidth拿到的值是70
spyOnProperty(nav['elementRef'].nativeElement, 'offsetWidth', 'get').and.returnValue(70);
spyOnProperty(nav['elementRef'].nativeElement, 'offsetHeight', 'get').and.returnValue(70);
spyOnProperty(nav['elementRef'].nativeElement, 'offsetLeft', 'get').and.returnValue(0);
spyOnProperty(nav['elementRef'].nativeElement, 'offsetTop', 'get').and.returnValue(0);
}
it('should show more btn when responsive and overflow', fakeAsync(() => {
fixture.debugElement.componentInstance.responsive = true;
fixture.detectChanges();
spyLinksAndNavOffset(fixture.componentInstance.links, fixture.componentInstance.nav);
dispatchFakeEvent(window, 'resize');
fixture.detectChanges();
tick(300);
fixture.detectChanges();
const moreBtn: DebugElement = fixture.debugElement.query(By.css('.thy-nav-more-container'));
expect(moreBtn).toBeTruthy();
}));
createSpy
- 当没有可监视的函数时,jasmine.createSpy 可以创建一个“裸”的spy。 这个spy充当任何其它spy - 跟踪调用、参数等。这个spy不会安装在任何地方,也不会在其背后有任何实现。
createSpy(name?: string, originalFn?: Function) → {Spy}
参数1:spy的名称。参数2:作为真正实现的函数。返回值是一个Spy。
// The example comes from Jasmine official
describe("A spy, when created manually", function() {
var whatAmI;
beforeEach(function() {
whatAmI = jasmine.createSpy('whatAmI');
whatAmI("I", "am", "a", "spy");
});
it("tracks that the spy was called", function() {
expect(whatAmI).toHaveBeenCalled();
});
});
实际应用:
// The example comes from an open source component library called ngx-tethys
it('should emit when dialog opening animation is complete', fakeAsync(() => {
const dialogRef = dialog.open(DialogSimpleContentComponent, {
viewContainerRef: testViewContainerRef
});
const spy = jasmine.createSpy('afterOpened spy');
dialogRef.afterOpened().subscribe(spy);
viewContainerFixture.detectChanges();
// callback should not be called before animation is complete
expect(spy).not.toHaveBeenCalled();
flush();
expect(spy).toHaveBeenCalledTimes(1);
}));
it('should close a dialog and get back a result', fakeAsync(() => {
const dialogRef = dialog.open(DialogSimpleContentComponent, {
viewContainerRef: testViewContainerRef
});
const afterClosedCallback = jasmine.createSpy('afterClosed callback');
dialogRef.afterClosed().subscribe(afterClosedCallback);
dialogRef.close('close result');
viewContainerFixture.detectChanges();
flush();
expect(afterClosedCallback).toHaveBeenCalledWith('close result');
expect(getDialogContainerElement()).toBeNull();
}));
createSpyObj
- 创建一个具有多个 spy 的对象。
createSpyObj(baseName?: string, methodNames: string[], propertyNames?: string[]) → {Object}
参数1:对象中spy的基本名称。参数2:spies的方法名数组,或者是[键是方法名称值是returnValue]的对象。参数3:spies的属性名数组,或者是[键是属性名值是returnValue]的对象- 你可以通过将数组或属性散列作为第三个参数传递给 createSpyObj 来快速创建具有多个属性的spy对象。 在这种情况下,你将没有对创建的spy的引用,因此如果以后需要更改它们的spy策略,必须使用
Object.getOwnPropertyDescriptor
方法。
// The example comes from Jasmine official
// 创建一个具有多个函数的spy对象。
describe("Multiple spies, when created manually", function() {
var tape;
beforeEach(function() {
tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);
tape.play();
tape.pause();
tape.rewind(0);
});
it("creates spies for each requested function", function() {
expect(tape.play).toBeDefined();
expect(tape.pause).toBeDefined();
expect(tape.stop).toBeDefined();
expect(tape.rewind).toBeDefined();
});
});
// 创建一个具有多个属性的spy对象。
it("creates a spy object with properties", function() {
let obj = createSpyObj("myObject", {}, { x: 3, y: 4 });
expect(obj.x).toEqual(3);
Object.getOwnPropertyDescriptor(obj, "x").get.and.returnValue(7);
expect(obj.x).toEqual(7);
});
参考资料
Angular官方测试文档:https://angular.cn/guide/testing
Jasmine从最简单的开始:https://jasmine.github.io/tutorials/your_first_suite
开源组件库:https://github.com/atinc/ngx-tethys