Escrever um teste do Jest para o serviço de conexão
Objetivos de aprendizagem
Após concluir esta unidade, você estará apto a:
- Listar os três adaptadores primários para serviços de conexão.
- Explicar os dados de simulação para o serviço de conexão.
- Entender as variáveis reativas e seus efeitos.
Testando o serviço @wire
Os componentes web do Lightning utilizam um serviço de conexão reativo criado no Lightning Data Service para ler os dados do Salesforce. Os componentes utilizam @wire
em sua classe JavaScript para ler os dados de um dos adaptadores de conexão nos módulos lightning/ui*Api
.
O serviço de conexão é parcialmente reativo porque suporta variáveis reativas. As variáveis reativas são prefixadas com um $
. Quando uma variável reativa muda, o serviço de conexão fornece novos dados. Se os dados existirem no cache do cliente, uma solicitação de rede pode não estar envolvida.
Usamos o utilitário de teste @salesforce/sfdx-lwc-jest
para testar como esses componentes tratam dados e erros do serviço de conexão.
O teste exige que você tenha total controle sobre a entrada que seu teste consome. Nenhum código externo ou dependências de dados são permitidos. Importamos a API de utilitário de teste de sfdx-lwc-jest
para simular os dados; assim, nosso teste não ficará dependente de fatores imprevisíveis como invocação remota ou latência do servidor.
Existem três adaptadores para simular dados de serviço de conexão.
- Adaptador de conexão genérico: O adaptador genérico emite dados sob demanda quando a API emit() é acionada. Ele não inclui informações adicionais sobre os próprios dados.
- Adaptador de conexão do Lightning Data Service (LDS): O adaptador LDS imita o comportamento do Lightning Data Service e inclui informações sobre as propriedades dos dados.
- Adaptador de conexão do Apex: O adaptador de conexão do Apex imita chamadas para um método do Apex e inclui qualquer status de erro.
Vamos analisar um decorador @wire
típico. Importe um adaptador de conexão usando a sintaxe de importação nomeada. Decore uma propriedade ou função com @wire
e especifique o adaptador de conexão. Cada adaptador de conexão define um tipo de dados.
Esse código importa o campo Account.Name e o utiliza no objeto de configuração de um adaptador de conexão.
import { LightningElement, api, wire } from 'lwc'; import { getRecord } from 'lightning/uiRecordApi'; import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name'; export default class Record extends LightningElement { @api recordId; @wire(getRecord, { recordId: '$recordId', fields: [ACCOUNT_NAME_FIELD] }) wiredRecord; }
Vamos analisar mais detalhadamente.
- A linha 8 está usando o decorador
@wire
para acessar o métodogetRecord
importado e passar a variável$recordId
reativa como seu primeiro argumento. O segundo argumento é uma referência aoAccount.Name
importado do esquema na linha 3. - A linha 9 pode ser uma função ou propriedade privada que recebe o fluxo de dados do serviço de conexão. Se for uma propriedade, os resultados são retornados para a propriedade de dados ou propriedade de erro da propriedade. Se for uma função, os resultados serão retornados em um objeto com uma propriedade de dados e uma propriedade de erro.
Agora vamos dar uma olhada nos diferentes adaptadores.
Usando o adaptador de conexão genérico
Primeiro, usamos o serviço @wire
com CurrentPageReference.
O serviço lightning-navigation oferece adaptadores e funções de conexão para gerar um URL ou navegar até uma referência de página. Vamos usar CurrentPageReference
para obter uma referência até a página atual no Salesforce e criar um teste.
- No Visual Studio Code, clique com o botão direto na pasta
LWC
e selecione SFDX: Criar componente web do Lightning. - Digite
wireCPR
como nome do novo componente. - Pressione Enter.
- Pressione Enter novamente para aceitar o local
force-app/main/default/lwc
padrão. - Na nova pasta
wireCPR/__tests__
, abra o arquivowireCPR.test.js
. - Substitua o novo arquivo por:
import { createElement } from 'lwc'; import WireCPR from 'c/wireCPR'; import { CurrentPageReference } from 'lightning/navigation'; // Mock realistic data const mockCurrentPageReference = require('./data/CurrentPageReference.json'); describe('c-wire-c-p-r', () => { afterEach(() => { while (document.body.firstChild) { document.body.removeChild(document.body.firstChild); } }); it('renders the current page reference in <pre> tag', () => { const element = createElement('c-wire-c-p-r', { is: WireCPR }); document.body.appendChild(element); // Select element for validation const preElement = element.shadowRoot.querySelector('pre'); expect(preElement).not.toBeNull(); // Emit data from @wire CurrentPageReference.emit(mockCurrentPageReference); return Promise.resolve().then(() => { expect(preElement.textContent).toBe( JSON.stringify(mockCurrentPageReference, null, 2) ); }); }); });
- Salve o arquivo e execute os testes.
Vamos analisar mais detalhadamente.
- A linha 3 tem uma nova importação:
CurrentPageReference
. - A linha 6 captura um arquivo com dados simulados de
PageReference
. Ainda não criamos isso, então essa é a primeira razão para o erro do teste.Test suite failed to run Cannot find module './data/CurrentPageReference.json' from 'wireCPR.test.js'
Corrigiremos o problema em seguida. - É na linha 26 que preenchemos os dados simulados usando
emit()
. - A linha 28 inicia a Promessa que espera que os dados simulados sejam atualizados no
preElement
.
Vamos criar o arquivo de dados de teste e atualizar o código para que o teste seja aprovado. Primeiro, crie um novo diretório no diretório __tests__
para armazenar o arquivo de dados simulado.
- Clique com o botão direito do mouse no diretório
__tests__
e selecione Nova pasta. - Digite
data
como nome do novo componente. - Pressione Enter.
- Clique com o botão direito do mouse no diretório
data
e selecione Novo arquivo. - Insira
CurrentPageReference.json
. - Pressione Enter.
- Insira o bloco de código json a seguir no novo arquivo:
{ "type": "standard__navItemPage", "attributes": { "apiName": "Wire" }, "state": {} }
- Salve o arquivo e execute os testes.
- O teste recebe essa mensagem de erro.
expect(received).not.toBeNull() Received: null
Excelente. Mesmo um teste com falha pode promover o progresso ao identificar rapidamente qualquer problema enquanto você trabalha com o código.
Em seguida, adicionamos o código HTML e JavaScript.
- Abra
wireCPR.html
. - Adicione o código a seguir dentro das marcas
template
:<lightning-card title="Wire CurrentPageReference" icon-name="custom:custom67"> <pre>{currentPageRef}</pre> </lightning-card>
- Salve o arquivo.
- Abra
wireCPR.js
e substitua o código pelo seguinte:import { LightningElement, wire } from 'lwc'; import { CurrentPageReference } from 'lightning/navigation'; export default class WireCPR extends LightningElement { @wire(CurrentPageReference) pageRef; get currentPageRef() { return this.pageRef ? JSON.stringify(this.pageRef, null, 2) : ''; } }
- Salve o arquivo e execute os testes.
- Os testes passam.
Vamos conferir o que está acontecendo. Quando o adaptador @wire
é usado, busca informações retornadas de um serviço. Precisamos criar uma simulação desses dados para utilizar em vez de fazer a chamada para o serviço a fim de obter os dados. Assim, continuamos a testar apenas os itens que temos atualmente e não aspectos fora de nosso escopo. Isso também ajuda a manter a rapidez dos testes.
Usando o adaptador de conexão do Lightning Data Service
Em seguida, utilizamos @wire
com o Lightning Data Service (LDS). O LDS concede acesso rápido a objetos personalizados e padrão. Nosso componente obtém dados do Salesforce usando o LDS e os exibe. Criaremos o teste para verificar se os dados são exibidos como esperado usando o adaptador LDS.
- Crie um novo componente web do Lightning no Visual Studio Code.
- Defina o nome como
wireLDS
. - Substitua o código no arquivo de teste
wireLDS.test.js
:import { createElement } from 'lwc'; import WireLDS from 'c/wireLDS'; import { getRecord } from 'lightning/uiRecordApi'; // Mock realistic data const mockGetRecord = require('./data/getRecord.json'); describe('c-wire-l-d-s', () => { afterEach(() => { while (document.body.firstChild) { document.body.removeChild(document.body.firstChild); } }); describe('getRecord @wire data', () => { it('renders contact details', () => { const element = createElement('c-wire-l-d-s', { is: WireLDS }); document.body.appendChild(element); // Emit data from @wire getRecord.emit(mockGetRecord); return Promise.resolve().then(() => { // Select elements for validation const nameElement = element.shadowRoot.querySelector('p.accountName'); expect(nameElement.textContent).toBe( 'Account Name: ' + mockGetRecord.fields.Name.value ); const industryElement = element.shadowRoot.querySelector('p.accountIndustry'); expect(industryElement.textContent).toBe( 'Industry: ' + mockGetRecord.fields.Industry.value ); const phoneElement = element.shadowRoot.querySelector('p.accountPhone'); expect(phoneElement.textContent).toBe( 'Phone: ' + mockGetRecord.fields.Phone.value ); const ownerElement = element.shadowRoot.querySelector('p.accountOwner'); expect(ownerElement.textContent).toBe( 'Owner: ' + mockGetRecord.fields.Owner.displayValue ); }); }); }); });
- Salve o arquivo e execute os testes.
- O teste falha devido à falta do arquivo de dados simulado que criamos a seguir.
Antes disso, vamos analisar o código de teste para ver o que está acontecendo.
- A linha 3 tem uma nova importação:
getRecord
.getRecord
está vindo da API LDS. - A linha 6 está simulando novamente os dados do arquivo
getRecord.json
no diretóriodata
. - A linha 23 usa o método emit em
getRecord
commockGetRecord
como um argumento. - A linha 25 inicia o retorno da
Promessa
e conferimos que vários elementos são atualizados com os dados simulados.
Em seguida, criamos o arquivo de dados simulado e o restante dos arquivos para obter um teste que seja aprovado. Executamos os testes depois que cada arquivo é criado para ver a progressão dos erros de teste até que sejam aprovados.
- Crie o diretório
data
no diretório__tests__
. - Crie o arquivo de dados de teste com o nome
getRecord.json
. - Adicione o código a seguir:
{ "apiName" : "Account", "childRelationships" : { }, "eTag" : "35f2effe0a85913b45011ae4e7dae39f", "fields" : { "Industry" : { "displayValue" : "Banking", "value" : "Banking" }, "Name" : { "displayValue" : null, "value" : "Company ABC" }, "Owner" : { "displayValue" : "Test User", "value" : { "apiName" : "User", "childRelationships" : { }, "eTag" : "f1a72efecde2ece9844980f21b4a0c25", "fields" : { "Id" : { "displayValue" : null, "value" : "005o0000000KEEUAA4" }, "Name" : { "displayValue" : null, "value" : "Test User" } }, "id" : "005o0000000KEEUAA4", "lastModifiedById" : "005o0000000KEEUAA4", "lastModifiedDate" : "2019-08-22T23:45:53.000Z", "recordTypeInfo" : null, "systemModstamp" : "2019-08-23T06:00:11.000Z" } }, "OwnerId" : { "displayValue" : null, "value" : "005o0000000KEEUAA4" }, "Phone" : { "displayValue" : null, "value" : "867-5309" } }, "id" : "0011J00001A3VFoQAN", "lastModifiedById" : "005o0000000KEEUAA4", "lastModifiedDate" : "2020-02-28T05:46:17.000Z", "recordTypeInfo" : null, "systemModstamp" : "2020-02-28T05:46:17.000Z" }
- Salve o arquivo e execute os testes.
- O teste falha.
- Abra
wireLDS.html
e insira o código a seguir entre as marcas template:<lightning-card title="Wire Lightning Data Service" icon-name="custom:custom108"> <template if:true={account.data}> <p class="accountName">Account Name: {name}</p> <p class="accountIndustry">Industry: {industry}</p> <p class="accountPhone">Phone: {phone}</p> <p class="accountOwner">Owner: {owner}</p> </template> <template if:true={account.error}> <p>No account found.</p> </template> </lightning-card>
- Salve o arquivo e execute os testes.
- O teste falha novamente, mas estamos quase lá. Você só precisa adicionar o controlador JavaScript para obter os dados.
- Abra
wireLDS.js
e substitua todo o código por:import { LightningElement, api, wire } from 'lwc'; import { getRecord, getFieldValue } from 'lightning/uiRecordApi'; import NAME_FIELD from '@salesforce/schema/Account.Name'; import OWNER_NAME_FIELD from '@salesforce/schema/Account.Owner.Name'; import PHONE_FIELD from '@salesforce/schema/Account.Phone'; import INDUSTRY_FIELD from '@salesforce/schema/Account.Industry'; export default class WireLDS extends LightningElement { @api recordId; @wire(getRecord, { recordId: '$recordId', fields: [NAME_FIELD, INDUSTRY_FIELD], optionalFields: [PHONE_FIELD, OWNER_NAME_FIELD] }) account; get name() { return getFieldValue(this.account.data, NAME_FIELD); } get phone() { return getFieldValue(this.account.data, PHONE_FIELD); } get industry(){ return getFieldValue(this.account.data, INDUSTRY_FIELD); } get owner() { return getFieldValue(this.account.data, OWNER_NAME_FIELD); } }
- Salve o arquivo e execute os testes.
- Os testes passam.
Mas e se houver um erro ao obter os dados? Também é possível testá-lo. Vamos adicionar um novo bloco describe em nosso arquivo de teste wireLDS.test.js
.
- Adicione o código a seguir logo após o bloco describe “getRecord @wire data” para que esteja dentro do bloco describe “c-wire-l-d-s”. É possível aninhar blocos describe para ajudar a clarificar os testes.
describe('getRecord @wire error', () => { it('shows error message', () => { const element = createElement('c-wire-l-d-s', { is: WireLDS }); document.body.appendChild(element); // Emit error from @wire getRecord.error(); return Promise.resolve().then(() => { const errorElement = element.shadowRoot.querySelector('p'); expect(errorElement).not.toBeNull(); expect(errorElement.textContent).toBe('No account found.'); }); }); });
- Salve o arquivo e execute os testes.
- Os testes passam porque o método
error()
está sendo utilizado nogetRecordAdapter
. Isso gera um erro nos dados simulados para queaccount.error
seja verdadeira.
Usando o adaptador de conexão do Apex
Em seguida, vamos nos aprofundar no Apex e ver como podemos usar @wire
para testá-lo.
A classe do Apex que o LWC está importando é considerada uma conexão externa que precisará ser simulada. Isso significa que podemos testar sem precisar criar a classe do Apex. Tudo o que precisamos fazer é simular a resposta esperada da chamada do Apex. Neste caso, estamos esperando exibir contas que são retornadas da classe do Apex. Criaremos testes que esperem que as contas sejam exibidas quando retornadas, ou uma mensagem se nenhuma for retornada.
Vamos criar o LWC que o utiliza.
- Crie um novo componente web do Lightning no Visual Studio Code.
- Defina o nome como
wireApex
. - Substitua o código no arquivo de teste
wireApex.test.js
:import { createElement } from 'lwc'; import WireApex from 'c/wireApex'; import getAccountList from '@salesforce/apex/AccountController.getAccountList'; // Realistic data with a list of contacts const mockGetAccountList = require('./data/getAccountList.json'); // An empty list of records to verify the component does something reasonable // when there is no data to display const mockGetAccountListNoRecords = require('./data/getAccountListNoRecords.json'); // Mock getAccountList Apex wire adapter jest.mock( '@salesforce/apex/AccountController.getAccountList', () => { const { createApexTestWireAdapter } = require('@salesforce/sfdx-lwc-jest'); return { default: createApexTestWireAdapter(jest.fn()) }; }, { virtual: true } ); describe('c-wire-apex', () => { afterEach(() => { while (document.body.firstChild) { document.body.removeChild(document.body.firstChild); } // Prevent data saved on mocks from leaking between tests jest.clearAllMocks(); }); describe('getAccountList @wire data', () => { it('renders six records', () => { const element = createElement('c-wire-apex', { is: WireApex }); document.body.appendChild(element); // Emit data from @wire getAccountList.emit(mockGetAccountList); return Promise.resolve().then(() => { // Select elements for validation const accountElements = element.shadowRoot.querySelectorAll('p'); expect(accountElements.length).toBe(mockGetAccountList.length); expect(accountElements[0].textContent).toBe(mockGetAccountList[0].Name); }); }); it('renders no items when no records are returned', () => { const element = createElement('c-wire-apex', { is: WireApex }); document.body.appendChild(element); // Emit data from @wire getAccountList.emit(mockGetAccountListNoRecords); return Promise.resolve().then(() => { // Select elements for validation const accountElements = element.shadowRoot.querySelectorAll('p'); expect(accountElements.length).toBe( mockGetAccountListNoRecords.length ); }); }); }); describe('getAccountList @wire error', () => { it('shows error panel element', () => { const element = createElement('c-wire-apex', { is: WireApex }); document.body.appendChild(element); // Emit error from @wire getAccountList.error(); return Promise.resolve().then(() => { const errorElement = element.shadowRoot.querySelector('p'); expect(errorElement).not.toBeNull(); expect(errorElement.textContent).toBe('No accounts found.'); }); }); }); });
- Salve o arquivo e execute os testes.
- Você recebe um erro para o arquivo de dados simulado ausente.
A maior parte do código é familiar. Há um novo item, jest.clearAllMocks()
, no código de limpeza para redefinir as simulações entre os testes. Isso é necessário porque temos dois arquivos fictícios para dois testes diferentes. O primeiro teste procura a chamada do Apex para enviar seis contas. O segundo teste declara o que acontece se nenhuma conta for encontrada. O último é o teste para declarar o que acontece se o Apex tiver um erro.
Vamos adicionar os arquivos de dados simulados e o restante do código.
- Crie o diretório
data
no diretório__tests__
. - Crie dois arquivos no novo diretório
data
chamadosgetAccountList.json
egetAccountListNoRecords.json
. - Insira o código abaixo em
getAccountList.json
:[ { "Id": "001o0000005w4fT", "Name": "Edge Communications" }, { "Id": "001o0000005w4fa", "Name": "United Oil & Gas Corporation" }, { "Id": "001o0000005w4fY", "Name": "Express Logistics and Transport" }, { "Id": "001o0000005w4fV", "Name": "Pyramid Construction Inc." }, { "Id": "001o0000005w4fX", "Name": "Grand Hotels & Resorts Ltd" }, { "Id": "001o000000k2NMs", "Name": "ABC Genius Tech Consulting" } ]
- O arquivo
getAccountListNoRecords.json
é preenchido com um objeto JSON em branco:[]
- Agora insira esse código entre as marcas
template
emwireApex.html
:<lightning-card title="Wire Apex" icon-name="custom:custom107"> <template if:true={accounts}> <template for:each={accounts} for:item="account"> <p key={account.Id}>{account.Name}</p> </template> </template> <template if:true={error}> <p>No accounts found.</p> </template> </lightning-card>
- Conclua ao substituir o código no
wireApex.js
com o seguinte:import { LightningElement, wire } from 'lwc'; import getAccountList from '@salesforce/apex/AccountController.getAccountList'; export default class WireApex extends LightningElement { accounts; error; @wire(getAccountList) wiredAccounts({ error, data }) { if(data) { this.accounts = data; this.error = undefined; } else if(error) { this.error = error; this.accounts = undefined; } } }
Observe que estamos obtendo apenas o métodogetAccountList
da classe do ApexAccountController
. Lembre-se: esse método precisa ser anotado com@AuraEnabled(cacheable=true)
para que funcione com LWCs. O@wire
o utiliza para preencher uma função comerror
oudata
retornados. - Salve todos os arquivos e execute os testes.
- Os testes passam.
Na próxima unidade, você abordará a simulação de outros componentes e concluirá as maneiras de testar os componentes web do Lightning com Jest.
Recursos
- Guia do desenvolvedor: Componentes web do Lightning: Usar o serviço de conexão para obter dados
- Guia do desenvolvedor: Componentes web do Lightning: adaptadores e funções de conexão do lightning/ui*Api
- Guia do desenvolvedor: Componentes web do Lightning: Escrever testes de Jest para componentes web do Lightning que utilizam o serviço de conexão
- Guia do desenvolvedor da API de interface do usuário: Obter um registro
- Extensões do Salesforce para Visual Studio Code: Componentes web do Lightning: Teste
- GitHub: salesforce/wire-service-jest-util
- GitHub: trailheadapps/lwc-recipes
- GitHub: wire-service-jest-util/docs/Migrating from version 2.x to 3.x