Перейти к содержанию

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

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

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

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

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

login-form.component.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Component({
    selector: 'info-message',
    template: `
        <h1>Message title</h1>

        <p>Message content</p>
    `,
})
export class InfoMessageComponent {
    constructor() {}
}

info-message.component.spec.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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.

1
expect(comp).toBeDefined();

Другое полезное свойство объекта ComponentFixturenativeElement. Значение свойства — объект типа HTMLElement. У объектов HTMLElement имеется метод querySelector, который по заданному селектору осуществляет поиск элементов в пределах шаблона компонента и также возвращает объект или массив объектов типа HTMLElement.

1
const h1 = infoMessageEl.querySelector('h1');

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

1
2
const infoMessageEl: HTMLElement =
    fixture.debugElement.nativeElement;

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

1
2
3
4
5
6
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-разметки, т. е. текст присутствовал в шаблоне еще до компиляции компонента. Но при задании значения вот так

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Component({
    selector: 'info-message',
    template: `
        <h1>{{ title }}</h1>

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

    constructor() {}
}

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

1
2
3
4
5
6
7
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 компиляция происходит автоматически.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
beforeEach(async(() => {
    TestBed.configureTestingModule({
        declarations: [InfoMessageComponent],
    })
        .compileComponents()
        .then(() => {
            fixture = TestBed.createComponent(
                InfoMessageComponent
            );
        });
}));

Асинхронный compileComponents() возвращает Promise и вызывается совместно с асинхронной функцией async() из библиотеки @angular/core/testing. Все синхронные операции после компиляции компонентов должны указываться в части then(), иначе будет сгенерировано исключение.

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

Ссылки

Комментарии