如何在 Angular 测试中使用 spy

简介: 如何在 Angular 测试中使用 spy

本文介绍了如何在Angular项目中使用Jasminespy进行函数模拟,以隔离组件测试,避免直接调用服务。通过spy监控函数调用并提供自定义返回值,确保组件测试的独立性。

简介

Jasmine spy 用于跟踪或存根函数或方法。spy 是一种检查函数是否被调用或提供自定义返回值的方法。我们可以使用spy 来测试依赖于服务的组件,并避免实际调用服务的方法来获取值。这有助于保持我们的单元测试专注于测试组件本身的内部而不是其依赖关系。

在本文中,您将学习如何在 Angular 项目中使用 Jasmine spy。

先决条件

要完成本教程,您需要:

  • 在本地安装 Node.js,您可以按照《如何安装 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 并用以下代码替换内容:

<h1>{{ incrementDecrement.value }}</h1>
<hr>
<button (click)="increment()" class="increment">Increment</button>
<button (click)="decrement()" class="decrement">Decrement</button>
<p class="message">
  {{ incrementDecrement.message }}
</p>

接下来,打开您的代码编辑器中的 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<AppComponent>;
  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<AppComponent>;
  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).toHaveBeenCalled();
  });
});

spyOn 接受两个参数:类实例(在本例中是我们的服务实例)和一个字符串值,表示要监视的方法或函数的名称。

在这里,我们还在 spy 上链接了 .and.callThrough(),这样实际方法仍然会被调用。在这种情况下,我们的 spy 只用于判断方法是否被调用以及监视参数。

以下是断言方法被调用两次的示例:

expect(incrementSpy).toHaveBeenCalledTimes(2);

以下是断言方法未被使用 'error' 参数调用的示例:

expect(incrementSpy).not.toHaveBeenCalledWith('error');

如果我们想避免实际调用服务上的方法,可以在 spy 上使用 .and.returnValue

我们的示例方法不适合这样做,因为它们不返回任何内容,而是改变内部属性。

让我们向服务添加一个实际返回值的新方法:

minimumOrMaximumReached() {
  return !!(this.message && this.message.length);
}

我们还向组件添加一个新方法,模板将使用它来获取值:

limitReached() {
  return this.incrementDecrement.minimumOrMaximumReached();
}

现在,如果达到限制,我们的模板将显示一条消息:

<p class="message" *ngIf="limitReached()">
  Limit reached!
</p>

然后,我们可以测试当达到限制时,我们的模板消息是否会显示,而无需实际调用服务上的方法:

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<AppComponent>;
  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。

如果您想了解更多关于 Angular 的知识,请查看我们的 Angular 主题页面,了解练习和编程项目。


目录
相关文章
|
SpringCloudAlibaba
SpringCloudAlibaba踩坑日记(二)Relying upon circular references is discouraged and they are prohibited by
SpringCloudAlibaba踩坑日记(二)Relying upon circular references is discouraged and they are prohibited by
4196 0
SpringCloudAlibaba踩坑日记(二)Relying upon circular references is discouraged and they are prohibited by
|
存储 Nacos 数据库
在 Docker 中部署 Nacos 并挂载配置文件
在 Docker 中部署 Nacos 并挂载配置文件
|
5月前
|
Kubernetes Linux Go
使用 Uber automaxprocs 正确设置 Go 程序线程数
`automaxprocs` 包就是专门用来解决此问题的,并且用法非常简单,只需要使用匿名导入的方式 `import _ "go.uber.org/automaxprocs"` 一行代码即可搞定。
249 78
|
Prometheus 监控 Cloud Native
Prometheus 安全性与数据隐私
【8月更文第29天】Prometheus 是一个开源的监控系统,广泛应用于各种规模的企业中。随着 Prometheus 的普及,确保其安全性变得尤为重要。本文将详细探讨如何确保 Prometheus 服务器的安全性,包括认证、授权、加密通信等方面的措施,并提供相应的配置示例。
598 2
|
11月前
|
机器学习/深度学习 自然语言处理 C++
TSMamba:基于Mamba架构的高效时间序列预测基础模型
TSMamba通过其创新的架构设计和训练策略,成功解决了传统时间序列预测模型面临的多个关键问题。
812 4
TSMamba:基于Mamba架构的高效时间序列预测基础模型
|
12月前
|
存储 Oracle 关系型数据库
【实操】单表数据量 200 GB,PostgreSQL 怎么应对??
【实操】单表数据量 200 GB,PostgreSQL 怎么应对??
440 1
|
Linux Docker 容器
docker启动完美容器的过程
本文详细介绍了使用Docker创建和管理容器的过程,包括拉取镜像、搜索镜像、创建容器、启动、停止、删除容器,以及查看容器日志和进程信息的常用命令。
564 2
|
监控 物联网 5G
物联网卡的一些主要类型
物联网卡(IoT SIM卡或物联网SIM卡)是专为物联网设备设计的SIM卡,它们允许设备连接到移动网络,进行数据传输和远程控制等操作。根据不同的需求和应用场景,物联网卡可以分为几种不同的类型。以下是物联网卡的一些主要类型及对应的操作简述:
|
Java 持续交付 Maven
Spring Boot程序的打包与运行:构建高效部署流程
构建高效的Spring Boot部署流程对于保障应用的快速、稳定上线至关重要。通过采用上述策略,您可以确保部署过程的自动化、可靠性和高效性,从而将专注点放在开发上面。无论是通过Maven的生命周期命令进行打包,还是通过容器技术对部署过程进行优化,选择正确的工具与实践是成功实现这一目标的关键。
430 2
|
Android开发
解決Android报错:Could not initialize class org.codehaus.groovy.reflection.ReflectionCache
解決Android报错:Could not initialize class org.codehaus.groovy.reflection.ReflectionCache
402 1