游乐游手机版
首页/前端开发/文章详情

Angular测试中Spy使用教程详解

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

简介

Jasmine spy 是一种用于跟踪或存根函数或方法的技术手段。简单来说,它允许你检查某个函数是否被调用,或者提供一个自定义返回值来替代实际执行。在 Angular 单元测试中,这尤为实用:当你的组件依赖某个服务时,通过 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 }}


{{ incrementDecrement.message }}

最后,修改 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 的实战技巧。

来源:https://www.jb51.net/javascript/318813z8n.htm
上一篇Angular NgTemplateOutlet创建可重用组件流程步骤 下一篇Angular项目利用拦截器处理HttpClient请求响应
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
checked表单属性与CSS变量实现换肤原理
前端开发 · 2026-07-02

checked表单属性与CSS变量实现换肤原理

先聊一个有意思的现象:不需要编写任何 JavaScript,仅靠一个 :checked 伪类,就能驱动整个主题切换系统。听起来很神奇,但原理其实并不复杂——核心在于,:checked 是浏览器原生状态的实时镜像,而不是 JS 模拟出来的开关。 用户点击 ,或者用键盘空格键选中它,状态更新的那一刻,C

HTML meta标签页面定时跳转实现
前端开发 · 2026-07-02

HTML meta标签页面定时跳转实现

说到前端开发中最简洁的页面跳转方式,meta http-equiv= "refresh " 绝对算得上一个经典方案。不过别看它结构简单,格式上稍有疏忽,页面就可能原地卡死,或者直接跳到一个错误地址。下面把几个最容易踩坑的细节彻底讲清楚,帮你避开这些常见陷阱。 使用 http-equiv= "refresh

Cypress跨测试用例状态传递的不推荐但可选方案
前端开发 · 2026-07-02

Cypress跨测试用例状态传递的不推荐但可选方案

Cypress 默认的设计哲学很干脆:每个测试用例都必须是独立小王国,谁也不靠谁。这意味着 it() 执行前,浏览器上下文会被“一键还原”——页面状态、LocalStorage、Cookies 统统清空,强制维护测试隔离。这一规则让很多新手头疼:明明前一个测试已经创建了员工,后一个测试怎么就没法直接

全面深度解析HTML主体main标签唯一性原则与使用规范
前端开发 · 2026-07-02

全面深度解析HTML主体main标签唯一性原则与使用规范

在进行前端无障碍审计时,不少开发者会遇到一个奇怪的场景:浏览器不报错,但Lighthouse却直接标红“duplicate-main”。这其实是语义层与渲染层之间的根本差异。 为什么浏览器不报错但 Lighthouse 直接标红 duplicate-main 关键原因就在于:`main` 是语义锚点

HTML main标签在文档结构中的唯一性详解
前端开发 · 2026-07-02

HTML main标签在文档结构中的唯一性详解

先做一个快速检测:打开你最近开发的一个页面,按下 Ctrl+F 搜索 。如果搜索结果里出现2个以上,那这篇文章建议你认真读完。 本期要聊的主题,是HTML标签中一个看似简单、实际极易踩坑的核心知识点:main标签的唯一性。很多开发者知道这个标签的存在,但真正写到项目里,尤其是用了React、Vue这