如何在Angular测试中使用间谍

来自菜鸟教程
跳转至:导航、​搜索

介绍

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

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

先决条件

要完成本教程,您需要:

  • Node.js 安装在本地,您可以按照【X57X】如何安装Node.js 并创建本地开发环境【X126X】进行。
  • 熟悉 设置 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 并将内容替换为以下代码:

src/app/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 并将内容替换为以下代码:

src/app/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 并将内容替换为以下代码:

src/app/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 并修改以下代码行:

src/app/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 获取对注入服务的引用。

以这种方式测试我们的组件是可行的,但也会对服务进行实际调用,并且我们的组件不是孤立地测试的。 接下来,我们将探索如何使用间谍来检查方法是否已被调用或提供存根返回值。

第 2 步 — 监视服务的方法

下面是使用 Jasmine 的 spyOn 函数调用服务方法并测试它是否被调用的方法:

src/app/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;
  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

我们的示例方法不是很好的候选方法,因为它们不返回任何内容,而是改变内部属性。

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

src/app/increment-decrement.service.ts

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

注意: 在表达式之前使用 !! 将值强制转换为布尔值。


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

src/app/app.component.ts

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

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

src/app/app.component.html

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

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

src/app/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;
  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 间谍。

如果您想了解有关 Angular 的更多信息,请查看 我们的 Angular 主题页面 以获取练习和编程项目。