Основы unit-тестирования в Angular

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

Читайте также: Шпаргалка по Angular CLI: основные команды и флаги

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

В этой статье мы поговорим о написании и запуске unit-тестов в Angular с помощью Jasmine и Karma.

Требования

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

1: Создание проекта

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

Эти файлы спецификаций следуют соглашению об именах *.spec.ts.

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

ng new angular-unit-test-example

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

cd angular-unit-test-example

Вместе с app.component здесь будет файл app.component.spec.ts. Откройте этот файл и проверьте его содержимое:

import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';

describe('AppComponent', () => {
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  });

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'angular-unit-test-example'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.componentInstance;
    expect(app.title).toEqual('angular-unit-test-example');
  });

  it('should render title', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('.content span').textContent).toContain('angular-unit-test-example app is running!');
  });
});

Как работает Jasmine?

Во-первых, нам нужно обсудить несколько вещей, которые важно знать о Jasmine:

  • блоки describe определяют набор тестов, каждый такой блок предназначен для отдельного теста.
  • beforeEach запускается перед каждым тестом и используется в части setup.
  • afterEach запускается после каждого теста и используется в teardown.
  • также можно использовать beforeAll и afterAll, они запускаются один раз до или после всех тестов.
  • проверить утверждение в Jasmine можно с помощью expect и сопоставителей типа toBeDefined, toBeTruthy, toContain, toEqual, toThrow, toBeNull, … Например: expect(myValue).toBeGreaterThan(3);
  • сделать утверждение отрицательным можно с помощью not: expect(myValue).not.toBeGreaterThan(3);
  • также можно писать пользовательские сопоставители.

TestBed — основная утилита для специального тестирования Angular. Мы будем использовать TestBed.configureTestingModule в блоке beforeEach; ему можно присвоить объект со значениями, аналогичными обычному NgModule для declarations, providers и imports. Затем вы можете связать вызов compileComponents, чтобы Angular мог скомпилировать объявленные компоненты.

Вы можете создать component fixture с помощью TestBed.createComponent. Фикстуры имеют доступ к debugElement, который, в свою очередь, даст вам доступ к внутренним компонентам фикстуры.

Изменения не отслеживаются автоматически, поэтому нам нужно вызвать detectChanges для фикстуры, чтобы Angular запустил обнаружение изменений.

Обертывание callback функции теста или первого аргумента beforeEach с помощью async позволяет Angular выполнять асинхронную компиляцию и подождать, пока содержимое внутри блока async не будет готово.

Как работают тесты?

Наш первый тест называется should create the app, и он использует expect для проверки наличия компонента с помощью toBeTruthy().

Второй тест называется should have as title ‘angular-unit-test-example’, и он использует expect, чтобы убедиться, что значение app.title равно строке ‘angular-unit-test-example’ с помощью toEqual().

Третий тест называется should render title, и он использует expect для проверки скомпилированного кода на наличие текста ‘angular-unit-test-example app is running!’ с помощью toContain().

В терминале выполните следующую команду:

ng test

Когда все три теста будут выполнены, вы увидите результат:

3 specs, 0 failures, randomized with seed 84683
AppComponent
* should have as title 'angular-unit-test-example'
* should create the app
* should render title

В настоящее время все три теста проходят успешно.

2: Создание тестового компонента

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

Откройте app.component.ts в редакторе кода и замените следующие строки кода логикой increment и decrement:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  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.html в редакторе кода и замените содержимое следующим кодом:

<h1>{{ value }}</h1>

<hr>

<button (click)="increment()" class="increment">Increment</button>

<button (click)="decrement()" class="decrement">Decrement</button>

<p class="message">
  {{ message }}
</p>

К этому моменту у вас должны быть пересмотренные версии app.component.ts и app.component.html.

3: Создание набора тестов

Снова откройте app.component.spec.ts в редакторе кода и замените его следующими строками кода:

import { TestBed, async, ComponentFixture } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { AppComponent } from './app.component';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let debugElement: DebugElement;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    debugElement = fixture.debugElement;
  }));

  it('should increment and decrement value', () => {
    fixture.componentInstance.increment();
    expect(fixture.componentInstance.value).toEqual(1);

    fixture.componentInstance.decrement();
    expect(fixture.componentInstance.value).toEqual(0);
  });

  it('should increment value 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 0 and show minimum message', () => {
    debugElement
      .query(By.css('button.decrement'))
      .triggerEventHandler('click', null);

    fixture.detectChanges();

    const message = debugElement.query(By.css('p.message')).nativeElement.innerText;

    expect(fixture.componentInstance.value).toEqual(0);
    expect(message).toContain('Minimum');
  });

  it('should stop at 15 and show maximum message', () => {
    fixture.componentInstance.value = 15;
    debugElement
      .query(By.css('button.increment'))
      .triggerEventHandler('click', null);
     
    fixture.detectChanges();

    const message = debugElement.query(By.css('p.message')).nativeElement.innerText;

    expect(fixture.componentInstance.value).toEqual(15);
    expect(message).toContain('Maximum');
  });
});

Мы назначаем фикстуру и debugElement непосредственно в блоке beforeEach, потому что они нужны всем нашим тестам. Также мы строго типизируем их, импортируя ComponentFixture из @angular/core/testing и DebugElement из @angular/core.

В нашем первом тесте мы вызываем методы самого экземпляра компонента.

Оставшиеся тесты мы используем DebugElement для запуска нажатия кнопок. Обратите внимание, у DebugElement есть метод query, который принимает предикат. Здесь мы используем утилиту By и ее метод css для поиска определенного элемента в шаблоне. DebugElement также имеет метод nativeElement для прямого доступа к DOM.

Кроме того, в последних 3 тестах мы используем fixture.detectChanges, чтобы Angular мог запускать обнаружение изменений, прежде чем он выполнит наши утверждения с помощью expect.

После внесения изменений запустите команду ng test:

ng test

Это запустит Karma в режиме просмотра, поэтому ваши тесты будут перекомпилироваться каждый раз при изменении файла.

4 specs, 0 failures, randomized with seed 27239
AppComponent
* should increment value in template
* should increment and decrement value
* should stop at 0 and show minimum message
* should stop at 15 and show maximum message

Все четыре теста будут пройдены успешно.

Заключение

В этой статье вы узнали, как писать и запускать unit-тесты в Angular с помощью Jasmine и Karma. Теперь, когда вы знакомы с основными утилитами тестирования Angular, вы можете начать писать тесты для простых компонентов.

Продолжайте обучение, тестируя компоненты с зависимостями, сервисами и т.д.

Вы также можете обратиться к официальной документации за подробным руководством по тестированию Angular.

Tags: , ,

Добавить комментарий