简介
Jasmine spy 是一种用于跟踪或存根函数或方法的技术手段。简单来说,它允许你检查某个函数是否被调用,或者提供一个自定义返回值来替代实际执行。在 Angular 单元测试中,这尤为实用:当你的组件依赖某个服务时,通过 spy 可以避免真实调用服务方法获取数据。这样一来,单元测试就能专注于测试组件自身的逻辑,而不是外部依赖的行为。

本教程将带你一步步掌握如何在 Angular 项目中使用 Jasmine spy 进行高效测试。
先决条件
开始前,你的本地环境需具备以下条件:
- 安装了 Node.js
- 熟悉搭建 Angular 项目的基本操作
本教程的验证环境为 Node v16.2.0、npm v7.15.1 和 @angular/core v12.0.4。
步骤 1 — 项目设置
我们从与 Angular 单元测试入门教程相似的示例开始。
先用 @angular/cli 创建一个新项目:
ng new angular-test-spies-example
然后进入刚创建的项目文件夹:
cd angular-test-spies-example
之前的示例通过两个按钮在 0 到 15 之间进行增减操作。这一次,我们将逻辑迁移到服务中,以便多个组件共享同一个中心值。
ng generate service increment-decrement
在编辑器中打开 increment-decrement.service.ts 文件,并替换为以下代码:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class IncrementDecrementService {
value = 0;
message!: string;
increment() {
if (this.value < 15) {
this.value += 1;
this.message = '';
} else {
this.message = 'Maximum reached!';
}
}
decrement() {
if (this.value > 0) {
this.value -= 1;
this.message = '';
} else {
this.message = 'Minimum reached!';
}
}
}
接着更新 app.component.ts 的内容:
import { Component } from '@angular/core';
import { IncrementDecrementService } from './increment-decrement.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor(public incrementDecrement: IncrementDecrementService) { }
increment() {
this.incrementDecrement.increment();
}
decrement() {
this.incrementDecrement.decrement();
}
}
再替换 app.component.html 的内容:
{{ incrementDecrement.value }}
最后,修改 app.component.spec.ts,同步更新测试代码:
import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { AppComponent } from './app.component';
import { IncrementDecrementService } from './increment-decrement.service';
describe('AppComponent', () => {
let fixture: ComponentFixture;
let debugElement: DebugElement;
let incrementDecrementService: IncrementDecrementService;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
providers: [ IncrementDecrementService ]
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
debugElement = fixture.debugElement;
incrementDecrementService = debugElement.injector.get(IncrementDecrementService);
}));
it('should increment in template', () => {
debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
fixture.detectChanges();
const value = debugElement.query(By.css('h1')).nativeElement.innerText;
expect(value).toEqual('1');
});
it('should stop at 15 and show maximum message', () => {
incrementDecrementService.value = 15;
debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
fixture.detectChanges();
const value = debugElement.query(By.css('h1')).nativeElement.innerText;
const message = debugElement.query(By.css('p.message')).nativeElement.innerText;
expect(value).toEqual('15');
expect(message).toContain('Maximum');
});
});
这里有一个小技巧:通过 debugElement.injector.get 可以获取注入的服务实例引用。
虽然这种方式可行,但每次操作都会真实调用服务方法,导致组件并未完全隔离测试。接下来,我们将学习如何使用 spy 来检查方法是否被调用,或者提供存根返回值。
步骤 2 —— 监视服务方法
下面的例子展示了如何使用 Jasmine 的 spyOn 函数来监视一个服务方法,并测试它是否被真正调用:
import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { AppComponent } from './app.component';
import { IncrementDecrementService } from './increment-decrement.service';
describe('AppComponent', () => {
let fixture: ComponentFixture;
let debugElement: DebugElement;
let incrementDecrementService: IncrementDecrementService;
let incrementSpy: any;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
providers: [ IncrementDecrementService ]
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
debugElement = fixture.debugElement;
incrementDecrementService = debugElement.injector.get(IncrementDecrementService);
incrementSpy = spyOn(incrementDecrementService, 'increment').and.callThrough();
}));
it('should call increment on the service', () => {
debugElement
.query(By.css('button.increment'))
.triggerEventHandler('click', null);
expect(incrementDecrementService.value).toBe(1);
expect(incrementSpy).toHa veBeenCalled();
});
});
spyOn 接收两个参数:第一个是类的实例(此处为服务实例),第二个是字符串,表示要监视的方法名称。
这里我们还链式调用了 .and.callThrough(),目的是让实际方法继续执行。这种情况下,spy 只负责判断方法是否被调用,并记录传入的参数。
如果要断言方法被调用了两次,可以这样写:
expect(incrementSpy).toHa veBeenCalledTimes(2);
如果想断言方法没有被传入 'error' 参数调用,则这样写:
expect(incrementSpy).not.toHa veBeenCalledWith('error');
如果我们希望避免真正执行服务里的方法,可以使用 .and.returnValue。不过刚才的例子中的方法并不适合这么做,因为它们不返回任何值,仅修改内部属性。
我们来给服务添加一个真正有返回值的新方法:
minimumOrMaximumReached() {
return !!(this.message && this.message.length);
}
同时给组件也添加一个方法,供模板调用:
limitReached() {
return this.incrementDecrement.minimumOrMaximumReached();
}
这样一来,我们就可以测试当达到限制时模板消息是否正确显示,而完全不需要真的去执行服务中的方法:
import { TestBed, waitForAsync, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { AppComponent } from './app.component';
import { IncrementDecrementService } from './increment-decrement.service';
describe('AppComponent', () => {
let fixture: ComponentFixture;
let debugElement: DebugElement;
let incrementDecrementService: IncrementDecrementService;
let minimumOrMaximumSpy: any;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
providers: [ IncrementDecrementService ]
}).compileComponents();
fixture = TestBed.createComponent(AppComponent);
debugElement = fixture.debugElement;
incrementDecrementService = debugElement.injector.get(IncrementDecrementService);
minimumOrMaximumSpy = spyOn(incrementDecrementService, 'minimumOrMaximumReached').and.returnValue(true);
}));
it(`should show 'Limit reached' message`, () => {
fixture.detectChanges();
const message = debugElement.query(By.css('p.message')).nativeElement.innerText;
expect(message).toEqual('Limit reached!');
});
});
结论
希望通过本教程,你已经掌握了在 Angular 项目中使用 Jasmine spy 的实战技巧。
