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



    Когда приложение разрастается или нам очень важно, чтобы оно работало верно при любом рефакторинге, мы начинаем задумываться о 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.
    • Избегайте плохих запахов.


    База знаний

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

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое