Модульное тестирование с помощью Jest


Jest — это фреймворк модульного тестирования JavaScript с открытым исходным кодом, созданный компанией Facebook и основанный на фреймворке Jasmine. Его отличием от конкурента Jasmine будет популярность, гибкость и скорость выполнения.

Общие сведения: Данная статья посвящена различным примерам рассуждений, ожиданий и матчеров для модульных тестов с Jest в среде, использующей фреймворк Angular SPA.

Мотивация Существует мало материалов, которые объясняют построчно сборку набора и написание сложных тестов.

Область применения: Эта статья рекомендуется пользователям, которые уже имеют концептуальную базу по теме модульных тестов в компонентах. Приведенные здесь примеры сложны, не доступны в репозитории, а также не посвящены установке инструмента, поэтому данный материал считается дополнительным к вводному пониманию фреймворка Jest. Тем не менее, была выстроена логическая структура, которая начинается с начальных концепций, подробно описывает сборку тестового набора в компоненте и заканчивается написанием/выполнением спецификации, фокусируясь на метрике увеличения тестового покрытия в SonarQube.

Цель: здесь мы проедем от 0 до 100 км очень быстро. Показывает, как планировать и писать спецификации, чтобы к концу вы могли действовать самостоятельно.

Установка

Я рекомендую установить помимо Jest еще и Jest-CLI, чтобы построить более подробный сценарий выполнения тестов, отвечающий вашим потребностям, ниже приведена ссылка для установки:

https://jestjs.io/pt-BR/docs/getting-started

В следующих темах будут объяснены некоторые важные концепции для конфигурации и написания модульных тестов.

Набор тестов

Они служат для определения масштаба того, что тестируется.

  • Внутри приложения есть несколько наборов тестов;
  • Примерами комплектов могут быть: математические расчеты, регистрация клиентов, запрос зарегистрированных…
  • В Jest набор — это глобальная функция Javascript под названием describe, которая имеет два параметра, которыми являются описание и тесты (specs).

Пример:

describe("Operação de Adição", () => { });
Войдите в полноэкранный режим Выход из полноэкранного режима

Технические характеристики

  • Спецификации — это тесты, которые проверяют набор тестов;
  • Как и сьюты, это глобальная функция Javascript под названием ‘it’, которая содержит два параметра, описание и функцию, соответственно;
  • Во втором параметре мы добавляем проверки (ожидания).

Пример:

it("deve garantir que 1 + 9 = 10", () => { });
Войдите в полноэкранный режим Выход из полноэкранного режима

Ожидания

  • Проверки служат для подтверждения результатов тестирования;
  • В Jest есть глобальная функция Javascript под названием ‘expect’, которая принимает один параметр в качестве аргумента, который является результатом, подлежащим проверке;
  • ‘expect’ должен использоваться в сочетании со сравнением (Matcher), которое будет содержать сравниваемое значение;
  • Спецификация может содержать одну или несколько проверок;
  • Хорошая практика заключается в том, чтобы всегда сохранять чеки по окончании функции.

Пример:

expect(Calculadora.adicionar(1, 9)).toBe(10);
Войдите в полноэкранный режим Выход из полноэкранного режима

Конфигурация тестового пакета

При написании тестов перед их запуском необходимо выполнить определенную работу по настройке. Если есть что-то, что должно быть выполнено многократно до или после для многих тестов, вы можете использовать hooks. Для данного примера мы будем использовать функцию, предоставляемую Jest: beforeEach, которая в основном будет повторять все, что обернуто ею, перед каждым выполненным тестом.

Как только мы создаем новый компонент с помощью angular CLI, автоматически генерируется файл spec с базовой конфигурацией. В соответствии с приведенным ниже кодом:

import { ComponentFixture, TestBed } from  '@angular/core/testing';
import { NovoComponent } from  './novo.component';
import { NovoModule } from  './novo.module';

describe('NovoComponent', () => {
    let  component: NovoComponent;
    let  fixture: ComponentFixture<NovoComponent>;

    beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [ NovoModule ],
            declarations: [],
            providers: []
        })
        .compileComponents();
        fixture = TestBed.createComponent(NovoComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
    });

    it('should create', () => {
        expect(component).toBeTruthy();
    });
});
Войдите в полноэкранный режим Выход из полноэкранного режима

Проанализируйте приведенный выше код. Мы заметили использование describe для создания набора тестов для NewComponent, мы видим, что есть две объявленные переменные component и fixture, в первой «typing» — имя класса, который был создан, во второй используется componentFixture для доступа к DOM, отладки и тестирования компонента. В следующей команде находится функция beforeEach, уже описанная ранее. Согласно конвенции Angular, мы принимаем, что каждый компонент должен обязательно содержаться в модуле, поэтому в структуре beforeEach мы всегда будем импортировать модуль, в котором объявлен тестируемый компонент. Вы должны добавить в providers зависимости, которые внедряются в файл typescript.

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

После компиляции этих компонентов с помощью compileComponents(), мы используем TestBed, который создает тестовый модуль Angular, который мы можем использовать для инстанцирования компонентов, выполнения инъекции зависимостей, чтобы настроить и инициализировать среду для тестирования. В следующей строке кода componentInstance используется для доступа к экземпляру класса корневого компонента, а fixture является оберткой для компонента и его шаблона. Функция fixture.detectChanges() будет срабатывать при любых изменениях, происходящих в DOM.
Наконец, модульные тесты будут добавлены с помощью структуры «it». В приведенном выше коде мы видим пример стандартного модульного теста, который проверяет, создается ли компонент. Крайне важно, чтобы в этот момент произошла первая проверка выполнения юнит-тестов, поскольку она покажет нам, правильно ли был собран набор тестов.

Услуги по высмеиванию

С этого момента я настоятельно рекомендую вам использовать функцию разделения экрана в вашем редакторе кода, так вы сможете видеть файл typescript с одной стороны, а файл spec — с другой.

Макет инжектируемых зависимостей позволит нам тестировать наш компонент изолированно, не беспокоясь о других зависимостях приложения. В теории, будет создан экземпляр объекта с «фальшивыми» данными, которые будут отражаться при каждом запросе зависимости.

Первое, что следует отметить в коде, это переменные, которые необходимо инициализировать, и зависимости, которые необходимо инжектировать:

import { Component, OnInit, ViewChild, ElementRef, OnDestroy } from  '@angular/core';
import { Subscription } from  'rxjs';

import { ChatOptionsQuery } from  'src/chat/store/chat-options/chat.options.query';

@Component({
    selector:  'app-chat-trigger',
    templateUrl:  './chat-trigger.component.html',
    styleUrls: ['./chat-trigger.component.scss'],
})
export class ChatTriggerComponent implements OnInit, OnDestroy  {
    totalPendingMessages = 0;
    maxMessagesCounter = 100
    chatTitle: string;
    chatMessage: string;
    openTooltip: boolean;
    ariaLabel:string;
    chatTitleSub$: Subscription;
    chatMessageSub$: Subscription;

    constructor(
        private chatOptionsQuery: ChatOptionsQuery,
        private appViewStore: AppViewStore,
    ) { }

    onHide(): void {
        this.appViewStore.update((state: AppViewState) => ({
            ...state,
            chatOpen: false,
            chatMinized: true,
            floatChat: true,
        }));
    }
Войдите в полноэкранный режим Выход из полноэкранного режима

Служба AppViewStore используется для вызова метода update на этом компоненте. В этот момент очень важно быть внимательным, потому что, как мы видим в приведенном ниже коде, при обращении к этой службе метод update отсутствует.

@Injectable({
providedIn: 'root'
})
@StoreConfig({ name: 'AppView' })
export class AppViewStore extends EntityStore<AppViewState> {
    constructor() {
        super(initialStateAppView);
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

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

export declare class EntityStore extends Store<S> {
    …
    update(newState: UpdateStateCallback<S>): any;
Войдите в полноэкранный режим Выход из полноэкранного режима

Для понимания этого сценария необходимо создать имитатор этих двух классов и добавить метод update в имитируемый класс со значением MockEntityStore.

В соответствии с конвенцией Angular, имя моков формируется из имени Mock + имя сервиса.

const MockAppViewStore = { };
const MockEntityStore = {
    update() {
        return true
    }
};

beforeEach(() => {
    TestBed.configureTestingModule({
        imports: [ ChatTriggerModule],
        declarations: [],
        providers: [
            { provide: AppViewStore, useValue: MockAppViewStore },
            { provide: EntityStore, useValue: MockEntityStore },
        ]
})
.compileComponents();
fixture = TestBed.createComponent(ChatTriggerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
Войдите в полноэкранный режим Выход из полноэкранного режима

Важно, чтобы имя высмеиваемого метода или переменной было идентичным. Но как показано в примере выше, параметр, переданный в методе update, не был высмеян, более того, было придумано булево возвращаемое значение. И этого достаточно для проведения тестов.

Создание модульных тестов на практике

Jest использует матчики для эффективного выполнения тестов. Для каждой конкретной ситуации в контексте тестирования существует несколько матчеров. Матчики реализуются путем вызова expect(). Чтобы вставить пример с более высокой сложностью, прежде всего, нам нужно понять концепцию и то, как реализовать mock-функции.

Макетные функции

  • Они позволяют создавать фиктивные функции и модули, имитирующие зависимость.
  • С помощью mock можно перехватить вызовы этой функции (и ее параметров) тестируемым кодом.
  • Позволяет перехватывать экземпляры функций конструктора при реализации с помощью new.
  • Позволяет настроить возвращаемые значения для тестируемого кода.

В других статьях часто можно встретить использование команды jest.fn() для создания имитационных функций, однако в данном файле используется синтаксис, аналогичный синтаксису Jasmine, поэтому имитационные функции будут создаваться с помощью команды Jest.spyOn(object, nameOfMethod), соединенной, например, с функцией mockImplementation, которая позволяет заменить исходную функцию.

Ниже мы приведем несколько примеров матчеров вместе с макетными функциями.

Пример

Мы будем использовать этот код в typescript в качестве основы для этого первого примера, чтобы протестировать хук жизненного цикла ngOnInit() Angular.

@Input('controls') controls: controls;
@Input("session") session: Session;

public floatChat$: Observable<boolean>;
public chatOpen$: Observable<boolean>;

public  joined: boolean;
public  joined$: Subscription;

constructor(
    public  appViewQuery: AppViewQuery,
) {
}

ngOnInit(): void {
    this.session = typeof  this.session == "string" ? JSON.parse(this.session) : this.session;
    this.controls = typeof  this.controls == "string" ? JSON.parse(this.controls) : this.controls;

    this.floatChat$ = this.appViewQuery.floatChat$;
    this.chatOpen$ = this.appViewQuery.chatOpen$;

    this.joined$ = this.appViewQuery.joined$.subscribe((data:boolean)=>{
        this.joined = data;
    });

    if (this.controls?.alwaysOpenChat) {
        this.onClickChatTrigger();
    }
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Пора изложить то, что было объяснено в начале статьи, этот первоначальный анализ чрезвычайно важен для определения плана действий по созданию тестов на ngOnInit(). В первых двух строках этого хука у нас есть два троичных if’а, которые используют переменные session и controls, имеющие свои собственные интерфейсы. Первый шаг — получить доступ к таким интерфейсам и создать их макет.

export interface Session {
  "contactId"?: string,
  "sessionId": string,
  "rede": string,
  "channel": channel,
  "nickname": string
}

export enum channel{
  "INTERNET_ON" = "INTERNET_ON",
  "INTERNET_OFF" = "INTERNET_OFF",
  "MOBILE_OFF" = "MOBILE_OFF", 
  "MOBILE_ON" = "MOBILE_ON"
}

export  interface  controls {
    alwaysOpenChat: boolean,
    buttonClose: boolean,
    nicknameChat?: string,
    nicknameAgent?: string,
    iconChat?: string,
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Мы будем добавлять такие моки глобально (доступ в любой структуре внутри этого spec-файла). Если в будущих тестах вам понадобится изменить какое-то значение, просто сделайте это внутри структуры it.
Для переменной session будут добавлены два макета, первый в формате строки, а второй в формате Object. Таким образом, JSON.parse может быть проверен внутри троичного «if».

describe('ChatComponent', () => {
    let component: ChatComponent;
    let fixture: ComponentFixture<ChatComponent>;

    const  mockSessionString: any = '{"contactId": "", "sessionId": "", "rede": "", "channel": "INTERNET_ON", "nickname": ""}';

    const mockSessionObject: Session = {
        contactId: '',
        sessionId: '',
        rede: '',
        channel: 'INTERNET_ON' as channel,
        nickname: ''
    };

    const mockControls: controls = {
        alwaysOpenChat: true,
        buttonClose: true,
        nicknameChat: '',
        nicknameAgent: '',
        iconChat: '',
    }
...
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Теперь давайте начнем редактировать спецификацию для этого крючка. Помня, что, как было настроено ранее, мы создали переменную component, которая ссылается на экземпляр тестируемого класса, поэтому мы присвоим созданные mocks экземпляру класса для этого конкретного теста:

fit('Should test ngOnInit', () => {
    component.session = mockSessionString;
    component.controls = mockControls;
    ...
}
Войдите в полноэкранный режим Выход из полноэкранного режима

Добавив букву «f» к структуре «it», при запуске тестов это будет единственная спецификация, тестируемая в этом наборе.

Продолжая анализ хуков, в следующих трех строках мы присваиваем двум наблюдаемым переменным типа boolean и одной типа «subscription()» значения зависимости AppViewQuery. На данном этапе нам нужно добавить такую зависимость в *провайдеры тестового пакета и, кроме того, добавить подражаемые переменные.

@Injectable({ providedIn:  'root' })
export  class  AppViewQuery  extends  QueryEntity<AppViewState> {
    floatChat$ =this.select("floatChat");
    chatOpen$ =this.select("chatOpen");
    joined$ =this.select("joined");
Войдите в полноэкранный режим Выход из полноэкранного режима

Когда мы наводим курсор мыши на метод, он показывает нам «тип» того, что возвращается, и для метода select() это Observable<boolean>, с этой информацией мы создадим mock, мы будем использовать функцию RxJS of():

const MockAppViewQuery = {
    floatChat$: of(false),
    chatOpen$: of(true),
    joined$: of(false)
};

beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [],
      declarations: [ChatComponent],
      providers: [
        { provide: AppViewQuery, useValue: MockAppViewQuery }
      ]
    })
Войдите в полноэкранный режим Выход из полноэкранного режима

Если посмотреть на остальную часть хука, у нас есть условие, которое для заданного нами сценария вернет true, потому что this.controls?.alwaysOpenChat существует. Таким образом, нам нужно будет передразнить метод, который находится внутри условного if(), для этого примера я использовал mockImplementation(), переписав (случайным образом) метод return в булево true:

fit('Should test ngOnInit', () => {
    component.session = mockSessionString;
    component.controls = mockControls;

    const spyOnClickChatTrigger = jest.spyOn(component, 'onClickChatTrigger').mockImplementation(()=> {
      return true;
    });
    ...
}
Войдите в полноэкранный режим Выход из полноэкранного режима

На данном этапе мы уже подготовили все строки спецификации ngOnInit(), осталось добавить проверки и команду для выполнения хука:

fit('Should test ngOnInit', () => {
    //PREPARAÇÃO
    component.session = mockSessionString;
    component.controls = mockControls;

    const spyOnClickChatTrigger = jest.spyOn(component, 'onClickChatTrigger').mockImplementation(()=> {
      return true;
    });

    //EXECUÇÃO
    component.ngOnInit(); //LINHA PARA EXECUTAR O HOOK

    //VERIFICAÇÃO
    expect(component.session).toEqual(mockSessionObject);
    expect(component.controls).toBe(mockControls);

    component.floatChat$.subscribe((res: boolean)=>{
      expect(res).toBeFalsy();
    });

    component.floatChat$.subscribe((res: boolean)=>{
      expect(res).toBeTruthy();
    });

    component.chatOpen$.subscribe(()=>{
      expect(component.joined).toBeFalsy();
      done();
    })

    expect(spyOnClickChatTrigger).toHaveBeenCalled();
 });
Войдите в полноэкранный режим Выход из полноэкранного режима

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

Для проверки числового равенства используются матчеры toBe и toEqual, где первый дополнительно сравнивает шрифт, а второй — только значения.

1-я проверка: сценарий настроен так, что переменная session проходит через JSON.parse() троичного «if». Таким образом, при сравнении с макетом в формате объекта он должен возвращать те же значения.

2-я проверка: сценарий был настроен так, чтобы переменная controls входила в ложное условие троичного «if» и возвращала тот же объект с той же типизацией.

Функции toBeTruthy и toBeFalsy проверяют, имеет ли переданный результат значение, которое может быть передано как true и false, соответственно, в if.

3-я, 4-я и 5-я проверки: для этих случаев нам нужно подписаться на наблюдаемые переменные, чтобы проверить, соответствует ли высмеиваемый возврат депеши AppViewQuery тому, что получено переменными floatChat$, chatOpen$ и joined. Для типа проверок с асинхронностью мы используем хитрость передачи 1 аргумента в функции «it» под названием done. После выполнения последней асинхронной проверки мы вызываем функцию done();, которая позволит сравнить ожидаемые результаты.

6-я проверка: макет переменной controls заполнен так, что он входит в структуру if(). Однако в данном случае мы создаем шпиона, который будет возвращать true при каждом вызове метода. Для этого случая мы можем провести различные тесты:

  1. проверить, является ли возврат переменной spy истинным, используя toBeTruthy();
  2. проверить, был ли вызван метод onClickChatTrigger(), используя функцию toHaveBeenCalled();
  3. проверить, был ли метод onClickChatTrigger() вызван 1 раз, используя функцию toHaveBeenCalledTimes(1). Мы выбрали вариант 2.

Теперь мы должны запустить набор тестов и проверить, успешно ли прошли тесты.

Выполнение

Базовой командой для запуска набора тестов является:

npm run test
Войдите в полноэкранный режим Выход из полноэкранного режима

Но когда Jest CLI установлен в проекте, он поддерживает camelCase и пунктирные аргументы, поэтому мы можем объединить 1 или более скриптов с приведенным выше кодом. Пример:

  • --detectOpenHandles
    Пытается собрать и распечатать обработчики, которые открыты и мешают Jest выйти из программы.

  • --silent
    Запрещает тестам печатать сообщения на консоли.

  • --coverage
    Указывает, что информация о сборе тестов должна собираться и сообщаться на консоли.

  • --ci
    Jest предполагает выполнение в среде CI (непрерывной интеграции). Изменение поведения при обнаружении нового моментального снимка. Вместо обычного поведения автоматического сохранения нового моментального снимка, тест будет провален и потребует запуска Jest с --updateSnapshot.

Чтобы запустить тесты только из вышеупомянутого файла, мы используем следующий синтаксис:

npm test -- Chat.component.spec.ts

результат будет:

 PASS  src/chat/Chat.component.spec.ts (119.938 s)
  ChatComponent
    √ Should test ngoninit (155 ms)
    ○ skipped Should test create component
    ○ skipped Should test ngOnChanges
    ○ skipped Should test ngAfterViewInit
    ○ skipped Should test load
    ○ skipped Should test hasAttribute
Войдите в полноэкранный режим Выход из полноэкранного режима

Мы понимаем, что наши тесты прошли успешно! Он игнорирует тесты других методов, потому что мы указали «fit» для спецификации ngOnInit().

Ссылки

https://jestjs.io/pt-BR/

https://cursos.alura.com.br/forum/topico-jasmine-x-jest-179443

https://www.devmedia.com.br/teste-unitario-com-jest/41234#:~:text=Jest%20%C3%A9%20один%20фреймворк%20из%20сообщества%20JavaScript.

Обзор и благодарности

Я благодарю Жуана Пауло Кастро Лима за идею и поддержку в создании этой статьи, а также моих друзей-рецензентов:

Элвес Гомеш Невеш Сантуш;
Франсис Гомеш Сантуш;
Матеус Винисиус Джеронимо Фалд;
Флавио Такеучи.

Оцените статью
devanswers.ru
Добавить комментарий