Jasmine测试中 spyOn、spyOnProperty、createSpy、createSpyObj

本文详细介绍了Jasmine中的spyOn、spyOnProperty用于函数和属性监视,createSpy和createSpyObj创建模拟对象的方法。通过实例演示了如何在Angular组件测试中使用这些工具,以及在不同场景下如何配置和检查spy的行为。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


最近在给组件补充测试,顺便整理一下常用的测试方法 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值