Поиск по сайту:

Как использовать шпионов в тестировании Angular


Введение

Шпионы Jasmine используются для отслеживания или заглушки функций или методов. Шпионы — это способ проверить, была ли вызвана функция, или предоставить собственное возвращаемое значение. Мы можем использовать шпионов для тестирования компонентов, которые зависят от службы, и избежать фактического вызова методов службы для получения значения. Это помогает сосредоточить наши модульные тесты на тестировании внутренних компонентов самого компонента, а не его зависимостей.

В этой статье вы узнаете, как использовать шпионов Jasmine в проекте Angular.

Предпосылки

Для выполнения этого урока вам понадобятся:

  • Node.js установлен локально, что можно сделать, следуя инструкциям по установке Node.js и созданию локальной среды разработки.
  • Некоторое знакомство с настройкой проекта Angular.

Это руководство было проверено с помощью Node v16.2.0, npm v7.15.1 и @angular/core v12.0.4.

Шаг 1 — Настройка проекта

Давайте воспользуемся примером, очень похожим на тот, который мы использовали в нашем введении в модульные тесты в Angular.

Сначала используйте @angular/cli для создания нового проекта:

  1. ng new angular-test-spies-example

Затем перейдите во вновь созданный каталог проекта:

  1. cd angular-test-spies-example

Раньше приложение использовало две кнопки для увеличения и уменьшения значений от 0 до 15.

В этом руководстве логика будет перемещена в службу. Это позволит нескольким компонентам получить доступ к одному и тому же центральному значению.

  1. 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.

Тестирование нашего компонента таким образом работает, но фактические вызовы также будут выполняться в службу, и наш компонент не тестируется изолированно. Далее мы рассмотрим, как использовать шпионов для проверки того, были ли вызваны методы, или для предоставления возвращаемого значения заглушки.

Шаг 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 принимает два аргумента: экземпляр класса (в данном случае экземпляр нашей службы) и строковое значение с именем метода или функции для слежения.

Здесь мы также привязали .and.callThrough() к шпиону, поэтому фактический метод все равно будет вызываться. Наш шпион в этом случае используется только для того, чтобы определить, действительно ли был вызван метод, и для слежения за аргументами.

Вот пример подтверждения того, что метод был вызван дважды:

expect(incrementSpy).toHaveBeenCalledTimes(2);

Вот пример подтверждения того, что метод не был вызван с аргументом error:

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

Если мы хотим избежать фактического вызова методов службы, мы можем использовать .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!');
  });
});

Заключение

В этой статье вы узнали, как использовать шпионов Jasmine в проекте Angular.

Если вы хотите узнать больше об Angular, ознакомьтесь с нашей темой по Angular, где вы найдете упражнения и проекты по программированию.