Примеры тестирования компонент


Компонент с вызовом асинхронного метода
Практически в каждом приложении имеются компоненты, которые зависимы от сервисов, которые хранят асинхронные методы, инициирующие HTTP-запросы к удаленному серверу и возвращающие определенные данные.

Поскольку основное назначение unit-тестов проверка не работоспособности API, а результата преобразования и (или) отображения полученных данных, то вызовы этих методов и их данных эмулируются через константы или Spy объекты.

info-message.component.ts


@Component({
selector: 'info-message',
template: `
<h1>Attention!</h1>

<p>{{ appService.message }}</p>
`,
})
export class InfoMessageComponent {
constructor(public appService: AppService) {}
}
info-message.component.spec.ts


describe('InfoMessageComponent', () => {
let fixture: ComponentFixture<InfoMessageComponent>

beforeEach(() => {
const appServiceStub = { message: 'Out of service' }

TestBed.configureTestingModule({
declarations: [InfoMessageComponent],
providers: [
{ provide: AppService, useValue: appServiceStub },
],
})

fixture = TestBed.createComponent(InfoMessageComponent)
})

it('should get message from AppService stub', () => {
fixture.detectChanges()
const infoMessageEl: HTMLElement =
fixture.debugElement.nativeElement
const p = infoMessageEl.querySelector('p')
expect(p.textContent).toContain('Out of service')
})
})
Не используйте реальные сервисы в тестах компонентов. Их внедрение может оказаться крайне сложным, или вообще невозможным, поскольку в сервисах могут быть проверки, которые пройдут только в реально работающем приложении.

В приведенном примере Angular тестирования константа appService предоставляет все необходимые данные и методы для их преобразования подобно тому, как это делает реальный сервис.

Для эмуляции асинхронных методов сервисов лучше подойдут Spy объекты.

app.service.ts


@Injectable({ providedIn: 'root' })
export class AppService {
constructor(private http: HttpClient) {}

getData(): Observable<any> {
return this.http.get('/api/data')
}
}
app.service.spec.ts


describe('InfoMessageComponent', () => {
let fixture: ComponentFixture<InfoMessageComponent>
let appService

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InfoMessageComponent],
providers: [AppService],
})

fixture = TestBed.createComponent(InfoMessageComponent)
appService = jasmine.createSpyObj('AppService', {
getData: 'Out of service',
})
})

it('should get message from AppService getData()', () => {
const comp = fixture.componentInstance
comp.message = appService.getData()
expect(comp.message).toBe('Out of service')
})
})
Объект Spy позволяет эмулировать обращение к асинхронному методу (название). Но сам тест выполняется синхронно, внутри него не выполняется никаких асинхронных действий.

При использовании в тестах Angular компонентов сервисов следует помнить, что Angular имеет иерархическое построение injector-ов. Так, если сервис был определен на уровне компонента, то при тестировании он должен быть взят не из корневого injector-а, а из injector-а самого компонента.


appService = fixture.debugElement.injector.get(AppService)
Но если сервис определен именно в модуле, т. е. находится в корневом injector-е, то можно использовать более простой в восприятии способ получения сервиса с использованием TestBed.get().


appService = TestBed.get(AppService)
Для асинхронности теста используйте функцию fakeAsync() библиотеки @angular/core/testing.


describe('InfoMessageComponent', () => {
let fixture: ComponentFixture<InfoMessageComponent>
let appService

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InfoMessageComponent],
providers: [AppService],
})

fixture = TestBed.createComponent(InfoMessageComponent)
appService = jasmine.createSpyObj('AppService', {
getData: 'Out of service',
})
})

it('should get message from AppService getData()', fakeAsync(() => {
const comp = fixture.componentInstance

setTimeout(
() => (comp.message = appService.getData()),
180
)

tick(180)

expect(comp.message).toBe('Out of service')
}))
})
Важным здесь является функция tick(), без которой использование fakeAsync() было бы бессмысленным. В качестве аргумента она принимает количество миллисекунд, на которое выполнение теста приостановиться. Так, в примере дальнейшие операции в тесте зависимы от вызова асинхронного метода (название), который выполняется за 180 миллисекунд.

Но данный подход не подойдет, если вы не знаете точное время исполнения метода. Самый простой пример - обращение к удаленному API, где время исполнения зависит от множества неконтролируемых факторов: стабильность соединения, пропускная способность канала, нагрузка на сервер и т. д. Здесь необходимо использовать функцию async().

info-message.component.ts


constructor(public appService: AppService){}

message: string = '';

ngOnInit(){
this.appService.getData().subscribe(message => this.message = message);
}
info-message.component.spec.ts


describe('InfoMessageComponent', () => {
let fixture: ComponentFixture<InfoMessageComponent>

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InfoMessageComponent],
providers: [AppService],
})

fixture = TestBed.createComponent(InfoMessageComponent)
})

it('should get message from AppService getData()', async(() => {
const comp = fixture.componentInstance
fixture.detectChanges() //Вызов ngOnInit()

fixture.whenStable().then(() => {
expect(comp.message).toBe('Out of service')
})
}))
})
async() запускает тест в специальной среде исполнения. Но главное здесь - метод whenSable(), возвращающий объект Promise, который выполнится после того, как очередь задач JavaScript станет пустой, т. е. будут исполнены все асинхронные и синхронные действия. Здесь отпадает необходимость в знании точного времени исполнения.

Взаимодействие компонентов
Когда вы передаете данные из одного компонента в другой через @Output() свойство, вам понадобится в тесте получить доступ к двум компонентам одновременно. Пример тестирования для такого случая.

parent.component.ts


@Component({
selector: 'parent-component',
template: `
<child-component
(message)="setMessage($event)"
></child-component>
`,
})
export class ParentComponent {
message: string = ''

constructor() {}

setMessage(text): void {
this.message = text
}
}
child.component.ts


@Component({
selector: 'child-component',
template: `
<div class="child">
<button (click)="sendMessage()">Send message</button>
</div>
`,
})
export class ChildComponent {
@Output() message: EventEmitter<any> = new EventEmitter<
any
>()

constructor() {}

sendMessage(): void {
this.message.emit('Child message')
}
}
parent.component.spec.ts


describe('ParentComponent', () => {
let fixture: ComponentFixture<ParentComponent>
let parentComp: ParentComponent

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, ChildComponent],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(ParentComponent)
parentComp = fixture.componentInstance
})
}))

it('should get message from ChildComponent', () => {
const childEl: HTMLElement = fixture.debugElement.nativeElement.query(
'.child'
)
childEl.click()

expect(parentComp.message).toBe('Child message')
})
})
Основное внимание здесь нужно сосредоточить на блоке beforeEach(). При конфигурации модуля TestingModule в части providers необходимо указать все компоненты, к которым происходит обращение в процессе тестирования. При этом явно создается именно экземпляр класса того компонента, который является родительским ко всем другим.

Доступ к дочерним компонентам осуществляется с помощью селекторов, применяемых относительно свойства nativeElement объекта debugElement с использованием функции querySelector(). В качестве селектора следует использовать id-атрибуты или классы, которые однозначно идентифицируют нужный компонент. Метод detectChanges() вызывается для привязки переданных значений в HTML-шаблоне.

Задать значения свойств дочернего компонента без явного создания экземпляра класса не получится.

Организация кода
Даже при тестировании небольшого компонента часто приходится производить много манипуляций с его свойствами или осуществлять выборку HTML-элементов по селекторам в пределах шаблона.

Поэтому для упрощения Angular тестирования и концентрации в одном месте методов, реализующих наиболее часто используемый функционал, общепринято создавать вспомогательный класс Page.

page.class.ts


class Page {
get links() {
return this.queryAll<HTMLElement>('a')
}

get inputs() {
return this.query<HTMLInputElement>('input')
}

fixture: ComponentFixture<TestComponent>

constructor(fixture: ComponentFixture<TestComponent>) {
this.fixture = fixture.componentInstance
}

private query<T>(selector: string): T {
return this.fixture.nativeElement.querySelector(
selector
)
}

private queryAll<T>(selector: string): T[] {
return this.fixture.nativeElement.querySelectorAll(
selector
)
}
}
При написании сценариев тестирования экземпляр класса Page создается в блоке beforeEach().


import { Page } from './page.ts'

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [TestComponent],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(TestComponent)
page = new Page(fixture)
})
}))