Skip to main content

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 imported getRecord method and passing in the reactive $recordId variable as its first argument. The second argument is a reference to the imported Account.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.

  1. In Visual Studio Code, right-click the lwc folder and select SFDX: Create Lightning Web Component.
  2. Enter wireCPR for the name of the new component.
  3. Press Enter.
  4. Press Enter to accept the default force-app/main/default/lwc.
  5. In the new wireCPR/__tests__ folder open the wireCPR.test.js file.
  6. 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)
          );
        });
      });
    });
  7. 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.

  1. Right-click the __tests__ directory and select New Folder.
  2. Enter data for the name of the new directory.
  3. Press Enter.
  4. Right-click the data directory and select New File.
  5. Enter CurrentPageReference.json.
  6. Press Enter.
  7. Enter the following json code block into the new file:
    {
      "type": "standard__navItemPage",
      "attributes": {
        "apiName": "Wire"
      },
      "state": {}
    }
  8. Save the file and run the tests.
  9. 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.

  1. Open wireCPR.html.
  2. Add the following code inside the template tags:
      <lightning-card title="Wire CurrentPageReference" icon-name="custom:custom67">
        <pre>{currentPageRef}</pre>
      </lightning-card>
  3. Save the file.
  4. 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) : '';
      }
    }
  5. Save the file and run the tests.
  6. 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.

  1. Create a new Lightning web component in Visual Studio Code.
  2. Set the name to wireLDS.
  3. 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
            );
          });
        });
      });
    });
  4. Save the file and run the tests.
  5. 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 the data directory.
  • Line 23 uses the emit method on getRecord with the mockGetRecord 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.

  1. Create the data directory in the __tests__ directory.
  2. Create the test data file with the name getRecord.json.
  3. 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"
    }
  4. Save the file and run the tests.
  5. The test fails.
  6. 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>
  7. Save the file and run the tests.
  8. The test fails again, but we're almost there. You just need to add the JavaScript controller to get the data.
  9. 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);
      }
    }
  10. Save the file and run the tests.
  11. The tests pass.
Note

Note

Lightning web components access Salesforce data and metadata from all custom and standard objects that User Interface API supports. External objects are not supported.

Get test data by grabbing a snapshot of data using a REST client to access the User Interface API. This approach is more accurate than writing the JSON by hand. Here is an example of the REST call to get the data above (you'll need your own account id):

/services/data/v47.0/ui-api/records/0011J00001A3VFo?fields=Account.Name,Account.Industry&optionalFields=Account.Phone,Account.Owner.Name

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.

  1. 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.');
          });
        });
      });
  2. Save the file and run the tests.
  3. The tests pass because you are using the error() method on the getRecordAdapter. This causes the mock data to error so the account.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.

  1. Create a new Lightning web component in Visual Studio Code.
  2. Set the name to wireApex.
  3. 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.');
          });
        });
      });
    });
  4. Save the file and run the tests.
  5. 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.

  1. Create the data directory in the __tests__ directory.
  2. Create two files in the new data directory named getAccountList.json and getAccountListNoRecords.json.
  3. 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"
      }
    ]
  4. The getAccountListNoRecords.json file gets filled with a blank JSON object:
    []
  5. Now enter this code between the template tags in wireApex.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>
  6. 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 the getAccountList method from the AccountController 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 the error or data returned.
  7. Save all the files and run the tests.
  8. The tests pass.

In the next unit, you tackle mocking other components and complete the ways of testing Lightning Web Components with Jest.

Resources

Comparta sus comentarios sobre Trailhead en la Ayuda de Salesforce.

Nos encantaría conocer su experiencia con Trailhead. Ahora puede acceder al nuevo formulario de comentarios cuando quiera desde el sitio de la Ayuda de Salesforce.

Más información Continuar para compartir comentarios