Write a Jest Test for Wire Service
Learning Objectives
After completing this unit, you'll be able to:
- List the three primary adapters for wire services.
- Explain mocking data for the wire service.
- Understand reactive variables and their effect.
Testing @Wire Service
Lightning web components use a reactive wire service built on Lightning Data Service to read Salesforce data. Components use @wire
in their JavaScript class to read data from one of the wire adapters in the lightning/ui*Api
modules.
The wire service is reactive in part because it supports reactive variables. Reactive variables are prefixed with a $
. When a reactive variable changes, the wire service provisions new data. If the data exists in the client cache, a network request may not be involved.
We use the @salesforce/sfdx-lwc-jest
test utility to test how these components handle data and errors from the wire service.
Testing requires that you have full control over the input that your test consumes. No outside code or data dependencies are allowed. We import the test utility API from sfdx-lwc-jest
to mock the data so our test isn't dependent on unpredictable factors like remote invocation or server latency.
There are three adapters for mocking wire service data.
- Generic wire adapter: The generic adapter emits data on demand when you call the emit() API. It does not include any extra information about the data itself.
- Lightning Data Service (LDS) wire adapter: The LDS adapter mimics Lightning Data Service behavior and includes information about the data's properties.
- Apex wire adapter: The Apex wire adapter mimics calls to an Apex method and includes any error status.
Let's look at a typical @wire
decorator. Import a wire adapter using named import syntax. Decorate a property or function with @wire
and specify the wire adapter. Each wire adapter defines a data type.
This code imports the Account.Name field and uses it in a wire adapter's configuration object.
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; }
Let's take a closer look.
- Line 8 is using the
@wire
decorator to access the importedgetRecord
method and passing in the reactive$recordId
variable as its first argument. The second argument is a reference to the importedAccount.Name
from the schema on Line 3. - Line 9 can be either a private property or a function that receives the stream of data from the wire service. If it's a property, the results are returned to the property's data property or error property. If it's a function, the results are returned in an object with a data property and an error property.
Now let's take a look at the different adapters.
Using the Generic Wire Adapter
First, we use the @wire
service with CurrentPageReference.
The lightning-navigation service offers wire adapters and functions to generate a URL or navigate to a page reference. We'll use CurrentPageReference
to get a reference to the current page in Salesforce and create a test for it.
- In Visual Studio Code, right-click the
lwc
folder and select SFDX: Create Lightning Web Component. - Enter
wireCPR
for the name of the new component. - Press Enter.
- Press Enter to accept the default
force-app/main/default/lwc
. - In the new
wireCPR/__tests__
folder open thewireCPR.test.js
file. - Overwrite the new file with:
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) ); }); }); });
- Save the file and run the tests.
Let's take a closer look.
- Line 3 has a new import:
CurrentPageReference
. - Line 6 grabs a file with mock
PageReference
data. We haven't created this yet so it is our first reason for the test to error.Test suite failed to run Cannot find module './data/CurrentPageReference.json' from 'wireCPR.test.js'
We fix this next. - Line 26 is where we populate the mock data using
emit()
. - Line 28 starts the Promise that expects the mock data to be updated into the
preElement
.
Let's create the test data file and update the code to get the test to pass. First, create a new directory under the __tests__
directory to store the mock data file.
- Right-click the
__tests__
directory and select New Folder. - Enter
data
for the name of the new directory. - Press Enter.
- Right-click the
data
directory and select New File. - Enter
CurrentPageReference.json
. - Press Enter.
- Enter the following json code block into the new file:
{ "type": "standard__navItemPage", "attributes": { "apiName": "Wire" }, "state": {} }
- Save the file and run the tests.
- The test gets this error message.
expect(received).not.toBeNull() Received: null
Excellent. Even a failing test can foster progress by quickly identifying any issues as you work through the code.
Next, we add the HTML and JavaScript code.
- Open
wireCPR.html
. - Add the following code inside the
template
tags:<lightning-card title="Wire CurrentPageReference" icon-name="custom:custom67"> <pre>{currentPageRef}</pre> </lightning-card>
- Save the file.
- Open
wireCPR.js
and replace the code with the following: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) : ''; } }
- Save the file and run the tests.
- The tests pass.
Let's see what's happening. When the @wire
adapter is used, it looks for information returned from a service. We need to create a mock of that data to use in place of actually making the call to the service to get the data. This keeps us testing only the items we currently have and not things outside our scope. This also helps to keep the tests fast.
Using the Lightning Data Service Wire Adapter
Next, we use @wire
with Lightning Data Service (LDS). LDS gives us quick access to custom and standard objects. Our component gets data from Salesforce using LDS and displays it. We'll create the test to verify that the data gets displayed as expected using the LDS adapter.
- Create a new Lightning web component in Visual Studio Code.
- Set the name to
wireLDS
. - Overwrite the code in the
wireLDS.test.js
test file: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 ); }); }); }); });
- Save the file and run the tests.
- The test fails due to the missing mock data file that we create next.
Before we do that, let's look at the test code to see what's happening.
- Line 3 has a new import:
getRecord
.getRecord
is coming from the LDS API. - Line 6 is mocking the data again from the
getRecord.json
file in thedata
directory. - Line 23 uses the emit method on
getRecord
with themockGetRecord
as an argument. - Line 25 starts the
Promise
return and we check that various elements are updated with the mock data.
Next, we create the mock data file and the rest of the files to get a passing test. We run the tests after each file is created to see the progression of the test errors until they pass.
- Create the
data
directory in the__tests__
directory. - Create the test data file with the name
getRecord.json
. - Add the following code:
{ "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" }
- Save the file and run the tests.
- The test fails.
- Open
wireLDS.html
and enter the following code between the template tags:<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>
- Save the file and run the tests.
- The test fails again, but we're almost there. You just need to add the JavaScript controller to get the data.
- Open
wireLDS.js
and overwrite all of it's code with: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); } }
- Save the file and run the tests.
- The tests pass.
But what if there is an error in getting the data? You can test for that as well. Let's add a new describe block in our test file wireLDS.test.js
.
- Add the following code right after the describe 'getRecord @wire data' block so it is inside the describe 'c-wire-l-d-s' block. You can nest describe blocks to help clarify tests.
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.'); }); }); });
- Save the file and run the tests.
- The tests pass because you are using the
error()
method on thegetRecordAdapter
. This causes the mock data to error so theaccount.error
will be true.
Using the Apex Wire Adapter
Next, let's dive into Apex and see how we can use @wire
to test it.
The Apex class the LWC is importing is considered an external connection that will need to be mocked. This means that we can test without needing to create the Apex class. All we need to do is mock the expected response from the Apex call. In this case, we are expecting to display Accounts that get returned from the Apex class. We'll create tests that expect the Accounts to be displayed when they are returned, and expect a message if none are returned.
Let's build the LWC that uses it.
- Create a new Lightning web component in Visual Studio Code.
- Set the name to
wireApex
. - Overwrite the code in the
wireApex.test.js
test file: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.'); }); }); }); });
- Save the file and run the tests.
- You get an error for the missing mock data file.
Most of the code is familiar. There is a new item, jest.clearAllMocks()
, in the cleanup code to reset the mocks between tests. This is needed because we have two mock files for two different tests. The first test is looking for the Apex call to deliver six accounts. The second test is asserting what would happen if there are no accounts found. Last is the test to assert what would happen if the Apex had an error.
Let's add the mock data files and the rest of the code.
- Create the
data
directory in the__tests__
directory. - Create two files in the new
data
directory namedgetAccountList.json
andgetAccountListNoRecords.json
. - Enter the code below into
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" } ]
- The
getAccountListNoRecords.json
file gets filled with a blank JSON object:[]
- Now enter this code between the
template
tags inwireApex.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>
- Finish by replacing the code in
wireApex.js
with this: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; } } }
Notice that we are only getting thegetAccountList
method from theAccountController
Apex class. Remember, that method has to be annotated with the@AuraEnabled(cacheable=true)
in order for it to work with LWCs. The@wire
uses it to populate a function with theerror
ordata
returned. - Save all the files and run the tests.
- The tests pass.
In the next unit, you tackle mocking other components and complete the ways of testing Lightning Web Components with Jest.
Resources
- Developer Guide: Lightning Web Components: Use the Wire Service to Get Data
- Developer Guide: Lightning Web Components: lightning/ui*Api Wire Adapters and Functions
- Developer Guide: Lightning Web Components: Write Jest Tests for Lightning Web Components That Use the Wire Service
- User Interface API Developer Guide: Get a Record
- Salesforce Extensions for Visual Studio Code: Lightning Web Components: Testing
- GitHub: salesforce/wire-service-jest-util
- GitHub: trailheadapps/lwc-recipes
- GitHub: wire-service-jest-util/docs/Migrating from version 2.x to 3.x