Тестирование компонент


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

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

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

Рассмотрим пример.

login-form.component.ts


@Component({
selector: 'login-form',
template: `
<form>
<input
type="text"
name="name"
[value]="loginForm.name"
/>
<input
type="password"
name="password"
[value]="loginForm.password"
/>
</form>

<button (click)="send()" [disabled]="!active">
Send
</button>
`,
})
export class LoginFormComponent implements OnInit {
@Input() active: boolean
@Output() validate: EventEmitter<any> = new EventEmitter<
any
>()

loginForm: any = {
name: '',
password: '',
}

constructor() {}

ngOnInit() {
this.loginForm.name = 'Bob'
this.loginForm.password = 'qwerty'
}

send() {
this.validate.emit(this.loginForm)
}
}
login-form.component.spec.ts


describe('LoginForm component', () => {
let comp

beforeEach(() => {
comp = new LoginFormComponent()
})

it('should set LoginForm values in OnInit', () => {
comp.ngOnInit()
expect(comp.loginForm.name).toBe('Bob', 'name value')
expect(comp.loginForm.password).toBe(
'qwerty',
'password value'
)
})

it('send() should raise LoginForm values', () => {
comp.ngOnInit()
comp.active = true

comp.validate.subscribe((credentials) => {
expect(comp.active).toBe(true, 'active')
expect(credentials).toBe(comp.loginForm, 'send event')
})

comp.send()
})
})
Если у компонента нет зависимостей, то экземпляр его класса в тесте создается с помощью ключевого слова new и без использования утилиты TestBed.

Первый тест проверяет установку значений формы в момент инициализации компонента, второй - возникновение события validate, инициируемое методом send().

Обратите внимание на то, как осуществляется проверка @Input() и @Output() свойств.

В процессе Angular component testing методы жизненного цикла не вызываются по умолчанию, как в реально работающем приложении. В тестах их вызов осуществляется явно.

Приступим к тестированию компонентов Angular с проверкой шаблона.

info-message.component.ts


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

<p>Message content</p>
`,
})
export class InfoMessageComponent {
constructor() {}
}
info-message.component.spec.ts


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

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

fixture = TestBed.createComponent(InfoMessageComponent)
})

it('should create', () => {
const comp = fixture.componentInstance
expect(comp).toBeDefined()
})

it('should contain "title"', () => {
const infoMessageEl: HTMLElement = fixture.nativeElement
const h1 = infoMessageEl.querySelector('h1')
expect(h1.textContent).toContain('title')
})
})
Метод createComponent() создает указанный компонент в DOM-дереве тестовой среды и возвращает объект типа ComponentFixture, через который можно получить доступ к экземпляру компонента используя свойство componentInstance и убедиться в том, что компонент инициализирован в DOM.


expect(comp).toBeDefined()
Другое полезное свойство объекта ComponentFixture - nativeElement. Значение свойства - объект типа HTMLElement. У объектов HTMLElement имеется метод querySelector, который по заданному селектору осуществляет поиск элементов в пределах шаблона компонента и также возвращает объект или массив объектов типа HTMLElement.


const h1 = infoMessageEl.querySelector('h1')
Свойства объекта nativeElement напрямую зависят от среды выполнения теста. Например, вне браузера DOM-эмуляция просто невозможна, например, в приложении Angular Universal, именно поэтому имеется свойство debugElement с объектом типа DebugElement в качестве значения. В объекте также имеется объект nativeElement, который работает универсально независимо от платформы. Поэтому рекомендуется при написании тестов придерживаться следующего формата:


const infoMessageEl: HTMLElement =
fixture.debugElement.nativeElement
Правда, если платформа не браузерная, то метод querySelector() не сработает. Аналогом являются query() и queryAll() объекта debugElement, принимающего результат, возвращаемый статическим методом css() класса By. Класс By входит в состав библиотеки @angular/platform-browser.


it('should contain "title"', () => {
const infoMessageEl: HTMLElement =
fixture.debugElement.nativeElement
const h1 = infoMessageEl.query(By.css('h1'))
expect(h1.textContent).toContain('title')
})
By.css() принимает селектор в формате, аналогичному в querySelector().

В последних примерах проверялось статическое содержимое элементов HTML-разметки, т. е. текст присутствовал в шаблоне еще до компиляции компонента. Но при задании значения вот так


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

<p>Message content</p>
`,
})
export class InfoMessageComponent {
title = 'Attention'

constructor() {}
}
тесты выполнились бы, поскольку createComponent() не связывает класс компонента с его шаблоном. Все переменные в таком случаем заменяются на пустые строки. Для инициации привязки необходимо вызвать detectChanges() у объекта, возвращаемого после вызова метода createComponent().


it('should contain "title"', () => {
fixture.detectChanges()
const infoMessageEl: HTMLElement =
fixture.debugElement.nativeElement
const h1 = infoMessageEl.querySelector('h1')
expect(h1.textContent).toContain('Attention')
})
Еще одна особенность приведенных ранее примеров - определение верстки в одном файле с определением класса. Но чаще всего (и это правильно) HTML-код и стили к нему выносятся в отдельные файлы. В таком случае необходимо вслед за методом configureTestingModule() вызвать compileComponents().

Принудительная компиляция необходима только если тестирование Angular компонентов выполняется вне среды CLI. В случае запуска через Angular CLI компиляция происходит автоматически.


beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [InfoMessageComponent],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(
InfoMessageComponent
)
})
}))
Асинхронный compileComponents() возвращает Promise и вызывается совместно с асинхронной функцией async() из библиотеки @angular/core/testing. Все синхронные операции после компиляции компонентов должны указываться в части then(), иначе будет сгенерировано исключение.

Вызов метода абсолютно безвреден. Обращение к compileComponents() без необходимости никак не повлияет на время и производительность тестирования.