Как использовать шпионов в тестировании 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
для создания нового проекта:
- 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
.
Тестирование нашего компонента таким образом работает, но фактические вызовы также будут выполняться в службу, и наш компонент не тестируется изолированно. Далее мы рассмотрим, как использовать шпионов для проверки того, были ли вызваны методы, или для предоставления возвращаемого значения заглушки.
Шаг 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, где вы найдете упражнения и проекты по программированию.