Юнит-тесты – неотъемлемая часть любого качественного процесса разработки. Если к Apex классам тесты обязательны, то для frontend – нет, хотя они и являются очень полезными для выявления некорректного рендера компонентов, а также их работы.
Jest – это фреймворк для тестирования, разработанный для обеспечения уверенности в правильной работе любого JavaScript кода. Написание Unit-тестов на Jest не занимает много времени и сил, но дает ощутимое преимущество в поиске неполадок в вашем LWC компоненте.
Что ж, перейдем от теории к практике.
Для удобства материал будет разбит на разделы:
- Подготовка к использованию Jest
- Добавление Jest в ваш проект
- Основной синтаксис и блоки
- Пример готового теста
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
И даже прямо из вкладки кода
Что ж, с этим разобрались и теперь можно переходить разбору структуры тестов и самых часто используемых функций.
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
}
]