如何在 Angular 测试中使用间谍

介绍

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

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

先决条件

要完成本教程,您需要:

本教程已通过 Node v16.2.0、npmv7.15.1 和@angular/corev12.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.

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

步骤 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 接受两个参数:类实例(在本例中是我们的服务实例)和一个字符串值,其中包含要监视的方法或函数的名称。

这里我们也链接.and.callThrough()了 spy,所以实际的方法仍然会被调用。在这种情况下,我们的间谍仅用于判断该方法是否被实际调用并监视参数。

下面是一个断言一个方法被调用两次的例子:

expect(incrementSpy).toHaveBeenCalledTimes(2);

这是一个断言没有使用参数调用方法的示例'error'

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

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

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

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

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 spies。

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

觉得文章有用?

点个广告表达一下你的爱意吧 !😁