Как стать автором
Обновить
Veeam Software
Продукты для резервного копирования информации

Angular: Интеграционное тестирование (Shallow testing)

Время на прочтение6 мин
Количество просмотров6.4K


Когда приложение разрастается или нам очень важно, чтобы оно работало верно при любом рефакторинге, мы начинаем задумываться о unit или e2e тестировании.

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

Далее попробуем разобраться с тестированием базового приложения на Angular и затронем немного теории.

Начнем с видов тестирования, которые могут производить разработчики приложений (в нашем случае frontend).



(Мартин Фаулер и Google Testing Blog)

У Angular такие методы есть “из коробки”.

Рассмотрим подробнее пирамиду тестирования на примере Angular- приложений.

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

Например, у нас есть компонент, в котором происходит конвертация hex строки в rgb, вывод этой информации пользователю, а также отправка на rest-сервер.

Если начнем смотреть в сторону unit-тестов, то мы можем на каждый метод в классе компонента написать множество тестов.

Если у нас есть метод rgbToHex(hex: string) => string, то для проверки метода с такой сигнатурой нам потребуется сделать следующее: expect(rgbToHex(‘777’)).toBe(‘rgb(119, 119, 119)’).

Вроде здорово, но у нас возникает проблема из-за большого количества функций, которые надо покрыть, и нам становится лень писать тесты. Кроме того, даже написав unit-тесты на каждый метод в компоненте, мы не гарантируем правильность их совместной работы.

Например, сделав и протестировав компонент вывода таблиц, мы можем вызвать его в компоненте “калькулятор”, но случайно указать неверный биндинг. Тогда данные не попадут в таблицы, и полностью рабочие компоненты не будут работать корректно.

image

Интеграционное тестирование — тестирование связки нескольких компонентов. С этого этапа мы начинаем тестировать не просто методы класса, но и их привязку к html, т.е. кликаем на элементы внутри компонента. В нотации Angular часто встречается Shallow testing, что по сути и является интеграционным тестированием.

На Shallow testing мы подробнее остановимся ниже.

E2E (end-to-end) тестирование — способ тестирования приложения полностью, чтобы решить проблемы unit-тестов.

При этом подходе мы пишем тестовые сценарии для полностью отрендеренного приложения, т.е. все компоненты и сервисы собраны воедино, и мы воспроизводим действия пользователя.
Это очень здорово, но у нас может появиться проблема динамического изменения стабов (обычно это статический json сервер на node.js), а для воспроизведения различных ситуаций нам могут потребоваться разные данные от эмулируемого сервера.

Например: у нас есть страница со списком пользователей, одним из компонентов этой панели является пагинация. Она должна вести себя по-разному при различном количестве пользователей. Но если данные на эмулируемом сервере заданы через json, то тогда количество пользователей будет всегда одним, и нам не удастся проверить все кейсы с различным количеством страниц.

В итоге можно прийти к выводу, что, если мы хотим гибко менять стабы и тестировать не по методам, а по логическим единицам интерфейса, нужно использовать что-то среднее между e2e и unit-тестами. Вот тут-то на помощь нам и приходит shallow testing (интеграционное тестирование).

Глоссарий:

  • Моки – это объекты для имитации ответов при различных use-case.
  • Стабы – это максимально глупые заглушки без логики (можно почитать Мартина).
  • Karma — тест-ранер, встроенный в angular (часто вместо него советуют jest, но сегодня не об этом).
  • Jasmine — фреймворк для описания тестов в виде спеков (см.ниже).
  • spec — расширение файла с тестами (описание спецификации в стиле BDD).
  • it — название методов для тестов в Jasmine.
  • xit — название методов, которые не будут запускаться.
  • fit — если в спеках есть методы с таким именем, то будут запущены только они.

Shallow testing для Аngular, как и для других фреймворков, – это подход unit-тестов, когда рендерится компонент размера достаточного, чтобы он мог существовать как отдельная единица ui со своей функциональностью.

Например, у нас есть компонент для конвертации hex -> rgb. Мы можем отрендерить только этот компонент, генерировать стабы для разных ситуаций, выполнить возможные use-case для этого компонента с точки зрения конечных пользователей и проверить работу компонента.

Давайте попробуем разобраться на примере (репозиторий).

Подготовим класс для доступа к элементам компонента в соответствии с PageObject и добавим хелпер в корень проекта.

Хелпер — будет помогать искать элементы в компонентах, которые были выбраны для рендеринга. Так можно облегчить жизнь, если мы используем Angular Material: тогда элементы типа select будут создавать список с option в отдельном блоке, и поиск этих элементов может привести к бойлерплейтам, а обертка в виде хелпера может помочь.

export class PageObjectBase {

  constructor(private root: HTMLDivElement) { }
  // Упрощаем доступ к input элементам компонентов
  _inputValue(cssSelector: string, value: string) { 
    if (value) {
      this.root.querySelector<HTMLInputElement>(cssSelector).value = value;
      this.root.querySelector<HTMLInputElement>(cssSelector).dispatchEvent(new Event('input'));
    }
    else {
      return this.root.querySelector<HTMLInputElement>(cssSelector).value
    }
  }
  // Теперь достаточно вызвать метод и передать селектор кнопки для клика
  _buttonClick(cssSelector: string) {
    this.root.querySelector<HTMLButtonElement>(cssSelector).dispatchEvent(new Event('click'));
  }

}

PageObject — популярный шаблон автоматизированного тестирования. Используется для упрощения поддержки написанных тестов. Если мы изменим UI, то тесты нам не придется переписывать, просто изменим селекторы элементов.


export class ConverterFromHexPageObject extends PageObjectBase {

  constructor(root: HTMLDivElement) {
    super(root)
  }

  hex(text?: string) {
    return this._inputValue('.input-hex', text);
  }

  rgb(text?: string) {
    return this._inputValue('.input-rgb', text);
  }

  clear() {
    this._buttonClick('.btn-clear')
  }

  calc() {
    this._buttonClick('.btn-calc')
  }

}

и сами тесты:


// Предположим, что сервис в компоненте должен работать с api по этому адресу
const urlToSave = 'http://localhost:4200/save-hex';


// Определим тест-сьют для тестирования компонента
describe('ConverterFromHexComponent', () => {
  let component: ConverterFromHexComponent;
  let fixture: ComponentFixture<ConverterFromHexComponent>;
  let page: ConverterFromHexPageObject;
  let httpTestingController: HttpTestingController;


  // Подготовительные действия перед каждым запуском теста, формируем тестовый модуль
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [ConverterModule, HttpClientTestingModule]
    })
      .compileComponents();
    httpTestingController = TestBed.get(HttpTestingController);
  }));


  // Настраиваем компонент, задаем начальные параметры
  beforeEach(() => {
    fixture = TestBed.createComponent(ConverterFromHexComponent);
    component = fixture.componentInstance;
    page = new ConverterFromHexPageObject(fixture.nativeElement);
    fixture.detectChanges();
  });


  // Хорошей практикой считается проверять, что не осталось зависших http запросов
  afterEach(() => {
    httpTestingController.verify();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should clear', async () => {
    page.hex('112233'); // в input ввели число
    expect(page.hex()).toBe('112233'); // проверили, что число на месте
    await page.clear(); // очищаем поле с помощью кнопки компонента
    fixture.detectChanges(); // запускаем цикл проверки изменений
    expect(page.hex()).toBe(''); // проверяем, что кнопка clear сработала
  });

  it('should convert', async () => {
    page.hex('123123');
    expect(page.rgb()).toBe('');
    page.calc();
    const req = httpTestingController.expectOne(urlToSave);
    expect(req.request.method).toEqual('POST');
    expect(req.request.body.hex).toEqual('123123');
    req.flush({});
    await fixture.detectChanges();
    expect(page.rgb()).toBe('rgb(18, 49, 35)');
  });

  it('should convert three-digit hex', async () => {
    page.hex('567');
    expect(page.rgb()).toBe('');
    page.calc();
    const req = httpTestingController.expectOne(urlToSave);
    expect(req.request.method).toEqual('POST');
    req.flush({});
    await fixture.detectChanges();
    expect(page.rgb()).toBe('rgb(85, 102, 119)');
  });

  it('rgb should be empty when entered incorrectly hex', async () => {
    page.hex('qw123we');
    page.calc();
    const req = httpTestingController.expectNone(urlToSave);
    await fixture.detectChanges();
    expect(page.rgb()).toBe('');
  });

});

Вроде все просто, но вот несколько интересных замечаний для Angular Shallow testing:

  • Всегда проверяйте, что не осталось незарезорвленных http запросов, это поможет точно понять, есть ли ненужные для нашей функциональности запросы.
  • Для больших компонентов можно воспользоваться PageObject паттерном, чтобы не копипастить селекторы к элементам.
  • Сделать хелпер для считывания json стабов и использовать их как заготовки стабов, которые во время теста можно изменять.
  • Настроить минимальный уровень покрытия тестами после покрытия основного функционала, чтобы держать планку с добавлением новой функциональности.
  • Использовать параллельное выполнение тестов.
  • Писать позитивные и негативные тесты.
  • Если есть работа с хранилищами, то надо чистить session, local, cookie перед каждым тестом (где это необходимо).
  • Избавиться от эффекта пестицида можно благодаря fake.js.
  • Избегайте плохих запахов.


База знаний

Репозиторий с примерами
Теги:
Хабы:
+7
Комментарии0

Публикации

Информация

Сайт
veeam.com
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Швейцария

Истории