Как использовать Jest юнит-тесты с LWC?

Юнит-тесты – неотъемлемая часть любого качественного процесса разработки. Если к Apex классам тесты обязательны, то для frontend – нет, хотя они и являются очень полезными для выявления некорректного рендера компонентов, а также их работы.

Jest – это фреймворк для тестирования, разработанный для обеспечения уверенности в правильной работе любого JavaScript кода. Написание Unit-тестов на Jest не занимает много времени и сил, но дает ощутимое преимущество в поиске неполадок в вашем LWC компоненте.

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

  1. Подготовка к использованию Jest
  2. Добавление Jest в ваш проект
  3. Основной синтаксис и блоки
  4. Пример готового теста

1.Подготовка к использованию Jest

В первую очередь необходимо установить Node.js, скачав его с официального сайта по ссылке https://nodejs.org/en/download/.

Далее проверить установился Node.js или нет можно классическим методом: использованием команды просмотра версии:

node --version

После чего, если все корректно работает, вам выведется версия установленного NodeJs. В моем случае это - v16.15.1 .

Далее заходим в созданный проект и приступаем к следующему пункту.

2.Добавление Jest в ваш проект

В терминале VS Code прописываем команду SFDX для добавления Jest в наш проект:

sfdx force:lightning:lwc:test:setup

Ниже видно результат выполнения

Затем создаем папку с названием ___tests___ нашего lwc компонента (у каждого компонента своя папка с его тестами). Название файлов с тестами должно иметь вид test_name.test.js .

Тесты можно запускать как из терминала командами:

npm run test:unit (для запуска всех тестов)
npm run test:unit:debug (для запуска всех тестов в режиме дебага)
npm run test:unit:coverage (для просмотра покрытия тестами кода)

Также их можно запускать из вкладки Тесты в VS Code

|272x427.16412900698316

И даже прямо из вкладки кода

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

3.Основной синтаксис и блоки

Блок импорт

Блок, в котором мы импортируем все необходимое для тестирования. Например, он может выглядеть вот так:

import { createElement } from 'lwc';

import Component from 'c/component';

import getOpportunities from '@salesforce/apex/OpportunityController.getOpportunities';

const mockRecordsList = require('./data/mockRecordsList.json');

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

Блок тестов

Блок, в котором и будет происходить основные действия и будет тестироваться логика. Для этого используется describe и он описан ниже :

describe('c-component', () => {

...

});

Далее будет представлено описание всех внутренних логических блоков, которые находятся внутри describe.

1)Блок пост- или предподготовки к тестам

Методы, которые будут выполняться перед или после тестов для приведения DOM-структуры и компонентов в первоначальный вид

2)Блок отдельного теста

it('component render', () => {

...

});

В блоке it и будет описываться каждый отдельный тест

1.1)Логика теста

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

Наиболее часто используемыми функциями здесь являются createComponent,querySelector и тригер ивента dispatchEvent

Например, может выглядеть так:

var element = createElement('c-component',{is: Component});

document.body.appendChild(element);

const handler = jest.fn();

element.addEventListener('onrowselection', handler);

const table = element.shadowRoot.querySelector('lightning-datatable');

table.dispatchEvent(new CustomEvent('onrowselection'));

Описание:

Создаем компонент с названием Component и добавляем его в DOM, далее создаем хандлер ивента onrowselection, который доступен в lightning-datatable по умолчанию. После этого берем из нашего компонента lightning-datatable и вызываем ивент вручную.

1.2)Блок проверки

Expect является аналогом assert и представляет из себя функцию проверки условий, например, можно использовать так:

expect(detailEls.length).toBe(mockRecordsList.length);

При использовании асинхронной логики и apex методов стоит использовать return Promise.resolve(); для того, чтоб вся логика и рендер успели завершиться до проверки.

Я далее буду использовать такой вариант:

Но также существует вариант без дополнительных объявлений:

return Promise.resolve().then(() => {

…

expect(...).toBe(...)

});

4.Пример готового теста

Тест будет представлен для простого компонента, который выводит Opportunity со статусом Closed Won, а именно их: Name(тэг <h1>) и Amount (тэг <p>)

import { createElement } from 'lwc';
import opportunityViewerWire from 'c/opportunityViewerWire';
import getClosedWonOpportunities from '@salesforce/apex/DataTableClosedWonController.getClosedWonOpportunities';

const mockRecordsList = require('./data/mockRecordsList.json');
const mockNoRecords = require('./data/mockNoRecords.json');

Импортируем метод createElement, наш компонент opportunityViewerWire, а также метод из нашего контроллера. Подгружаем замоканые данные из папки data, которая находится в папке ___tests___ в виде json файлов.

jest.mock(   '@salesforce/apex/DataTableClosedWonController.getClosedWonOpportunities',
    () => {
        const {
            createApexTestWireAdapter
        } = require('@salesforce/sfdx-lwc-jest');
        return {
            default: createApexTestWireAdapter(jest.fn())
        };
    },
    { virtual: true }
);

Мокаем поведение нашего wire метода через адаптер, чтоб далее имитировать его работу через замоканые данные из json файлов.

describe('c-apex-wire-method-to-function', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });


    async function flushPromises() {
        return Promise.resolve();
    }

    describe('getClosedWonOpportunities @wire', () => {
        it('renders when data returned', async () => {
            const element = createElement('c-apex-wire-method-to-function', {
                is: opportunityViewerWire
            });
            document.body.appendChild(element);

            getClosedWonOpportunities.emit(mockRecordsList);

      
            await flushPromises();

           
            const detailEls = element.shadowRoot.querySelectorAll('h1');
            expect(detailEls.length).toBe(mockRecordsList.length);
            expect(detailEls[0].textContent).toBe(mockRecordsList[0].Name);
        });


        it('renders when data dont returned', async () => {
            const element = createElement('c-apex-wire-method-to-function', {
                is: opportunityViewerWire
            });
            document.body.appendChild(element);

            getClosedWonOpportunities.emit(mockNoRecords);

            await flushPromises();

            const detailEls = element.shadowRoot.querySelectorAll('h1');
            expect(detailEls.length).toBe(mockNoRecords.length);
        });
       
    });

});

Далее идет код тестов, которые тестируют отображение на компоненте данных. Эти данные приходят из Apex’а.

Данные тесты на 100% покрывают код и отражают негативный и позитивный варианты в работе компонента.

В заключение этого краткого обзора использования Jest для тестирования LWC компонентов хочу сказать, что пока Unit-тесты для LWC только набирают свою популярность, но, по моему мнению, их желательно начинать использовать уже сейчас так, как они являются мощным и простым инструментом для выявления неполадок в работе, а также, на примере GearSet(CI/CD), смогут встраиваться в процесс деплоинга и запускаться перед каждым деплоем (сейчас эта функция в beta) наравне с обычными Apex тестами.

Дополнительные материалы

HTML
<template>
    <lightning-card
        title="View Opportunities Closed Won"
        icon-name="standard:opportunity"
    >
        <div class="slds-var-m-around_medium">
            <template if:true={opportunities}>
                <template for:each={opportunities} for:item="opportunity">
                    <h1 key={opportunity.Id}>{opportunity.Name}</h1>
                    <p key={opportunity.Id}>{opportunity.Amount}</p>
                </template>
            </template>
        </div>
    </lightning-card>
</template>
JS
import { LightningElement, wire } from 'lwc';
import getClosedWonOpportunities from '@salesforce/apex/DataTableClosedWonController.getClosedWonOpportunities';

export default class ContactViewerWire extends LightningElement {
    opportunities;

    @wire(getClosedWonOpportunities)
    wiredOpportunities({ data }) {
        if (data) {
            this.opportunities = data;

        }
    }
}
opportunityViewerWire.test.js(полный тест без описания)
import { createElement } from 'lwc';
import opportunityViewerWire from 'c/opportunityViewerWire';
import getClosedWonOpportunities from '@salesforce/apex/DataTableClosedWonController.getClosedWonOpportunities';

const mockRecordsList = require('./data/mockRecordsList.json');
const mockNoRecords = require('./data/mockNoRecords.json');

jest.mock(   '@salesforce/apex/DataTableClosedWonController.getClosedWonOpportunities',
    () => {
        const {
            createApexTestWireAdapter
        } = require('@salesforce/sfdx-lwc-jest');
        return {
            default: createApexTestWireAdapter(jest.fn())
        };
    },
    { virtual: true }
);

describe('c-apex-wire-method-to-function', () => {
    afterEach(() => {
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });


    async function flushPromises() {
        return Promise.resolve();
    }

    describe('getClosedWonOpportunities @wire', () => {
        it('renders when data returned', async () => {
            // Create initial element
            const element = createElement('c-apex-wire-method-to-function', {
                is: opportunityViewerWire
            });
            document.body.appendChild(element);

            getClosedWonOpportunities.emit(mockRecordsList);

           
            await flushPromises();

           
            const detailEls = element.shadowRoot.querySelectorAll('h1');
            expect(detailEls.length).toBe(mockRecordsList.length);
            expect(detailEls[0].textContent).toBe(mockRecordsList[0].Name);
        });

        it('renders when data dont returned', async () => {
            const element = createElement('c-apex-wire-method-to-function', {
                is: opportunityViewerWire
            });
            document.body.appendChild(element);

            getClosedWonOpportunities.emit(mockNoRecords);

            await flushPromises();

            const detailEls = element.shadowRoot.querySelectorAll('h1');
            expect(detailEls.length).toBe(mockNoRecords.length);
        });
       
    });

});
DataTableClosedWonController.cls
public with sharing class DataTableClosedWonController {
    private static String CLOSED_WON='Closed Won';
    @AuraEnabled(cacheable=true)
    public static List<Opportunity> getClosedWonOpportunities(){
        return[SELECT Id,Amount,Name FROM Opportunity WHERE StageName=:CLOSED_WON WITH SECURITY_ENFORCED];
    }
}
mockNoRecords.json
[]
mockRecordsList.json
[
    {
        "Id": "111111",
        "Name": "Generator",
        "Amount": 120000
    },
    {
        "Id": "222222",
        "Name": "Panel",
        "Amount": 2000000
    },
    {
        "Id": "333333",
        "Name": "Vehicle",
        "Amount": 40000
    },
    {
        "Id": "444444",
        "Name": "Generator2",
        "Amount": 500000
    },
    {
        "Id": "555555",
        "Name": "Panel2",
        "Amount": 60000
    },
    {
        "Id": "666666",
        "Name": "Vehicle2",
        "Amount": 700000
    }
]
2 Likes