進行状況の追跡を始めよう
Trailhead のホーム
Trailhead のホーム

ワイヤサービスの Jest テストの記述

学習の目的

この単元を完了すると、次のことができるようになります。

  • ワイヤサービスの主なアダプタを 3 つ挙げる。
  • ワイヤサービスのデータのモックについて説明する。
  • リアクティブ変数とその影響を理解する。

@wire サービスのテスト

Lightning Web コンポーネントは、Lightning データサービス上に構築されたリアクティブワイヤサービスを使用して、Salesforce データを読み取ります。コンポーネントは JavaScript クラスの @wire を使用して、lightning/ui*Api モジュールのいずれかのワイヤアダプタからデータを読み取ります。

このワイヤサービスがリアクティブである理由の 1 つは、リアクティブ変数をサポートしていることです。リアクティブ変数には $ のプレフィックスが付いています。リアクティブ変数が変更されると、ワイヤサービスが新しいデータをプロビジョニングします。データがクライアントのキャッシュに存在する場合は、ネットワークリクエストを伴わないことがあります。 

ここでは、@salesforce/sfdx-lwc-jest テストユーティリティを使用して、これらのコンポーネントがワイヤサービスからのデータやエラーをどのように処理するかをテストします。

テストをする場合、テストで使用する入力を完全に制御できることが求められます。外部のコードやデータとの連動関係は認められません。ここでは、sfdx-lwc-jest からデータをモックするテストユーティリティ API をインポートして、テストがリモート呼び出しやサーバ遅延といった予測不能な要因に左右されないようにします。

ワイヤサービスのデータのモックに使用するアダプタには次の 3 種類があります。 

  • 汎用ワイヤアダプタ: emit() API をコールすると、汎用アダプタがデータをオンデマンドで生成します。データ自体に関する追加情報は含まれません。
  • Lightning データサービス (LDS) ワイヤアダプタ: LDS アダプタは Lightning データサービスの動作を模倣します。データのプロパティに関する情報が含まれます。
  • Apex ワイヤアダプタ: Apex ワイヤアダプタは Apex メソッドへのコールを模倣します。エラー状況が含まれます。

では、典型的な @wire デコレータを見てみましょう。指定したインポート構文を使用して、ワイヤアダプタをインポートします。プロパティや関数を @wire でデコレートして、ワイヤアダプタを指定します。各ワイヤアダプタがデータ型を定義します。

次のコードは、Account.Name 項目をインポートして、ワイヤアダプタの設定オブジェクトで使用します。

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;
}

詳しく見てみましょう。 

  • 8 行目は @wire デコレータを使用して、インポート済みの getRecord メソッドにアクセスし、$recordId リアクティブ変数を 1 つ目の引数として渡しています。2 つ目の引数は、3 行目のスキーマからインポートされた Account.Name への参照です。
  • 9 行目は、ワイヤサービスからデータのストリームを受信する非公開のプロパティまたは関数です。プロパティの場合は、結果がそのプロパティの data プロパティまたは error プロパティに返されます。関数の場合は、結果が data プロパティと error プロパティが指定されているオブジェクトで返されます。

では、各種のアダプタを見ていきましょう。

汎用ワイヤアダプタの使用

最初に、CurrentPageReference を設定した @wire サービスを使用します。

lightning-navigation サービスには、URL を生成したり、ページ参照に移動したりするためのワイヤアダプタや関数が用意されています。ここでは CurrentPageReference を使用して Salesforce の現在のページへの参照を取得し、そのテストを作成します。 

  1. Visual Studio Code で、lwc フォルダを右クリックし、[SFDX: Create Lightning Web Component (SFDX: Lightning Web コンポーネントの作成)] を選択します。
  2. 新しいコンポーネントの名前に「wireCPR」と入力します。
  3. Enter キーを押します。
  4. Enter キーを押して、デフォルトの force-app/main/default/lwc を受け入れます。
  5. 先ほど作成した wireCPR ディレクトリを右クリックし、[New Folder (新しいフォルダ)] を選択します。
  6. 「__tests__」と入力します。
  7. Enter キーを押します。
  8. __tests__ ディレクトリを右クリックし、[New File (新しいファイル)] を選択します。
  9. 「wireCPR.test.js」と入力します。
  10. Enter キーを押します。
  11. 新しいファイルに次のコードブロックを入力します。
    import { createElement } from 'lwc';
    import WireCPR from 'c/wireCPR';
    import { CurrentPageReference } from 'lightning/navigation';
    import { registerTestWireAdapter } from '@salesforce/sfdx-lwc-jest';
      
    // Mock realistic data
    const mockCurrentPageReference = require('./data/CurrentPageReference.json');
      
    // Register a standard test wire adapter.
    const currentPageReferenceAdapter = registerTestWireAdapter(
      CurrentPageReference
    );
      
    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
        currentPageReferenceAdapter.emit(mockCurrentPageReference);
      
        return Promise.resolve().then(() => {
          expect(preElement.textContent).toBe(
            JSON.stringify(mockCurrentPageReference, null, 2)
          );
        });
      });
    });
  12. ファイルを保存して、テストを実行します。

詳しく見てみましょう。

  • 3 ~ 4 行目は、CurrentPageReferenceregisterTestWireAdapter の 2 つの新しいインポートです。
  • 7 行目は、PageReference というモックデータを記載したファイルを取得します。このファイルはまだ作成していないため、テストでエラーが発生する 1 つ目の理由になります。
    Test suite failed to run
      Cannot find module './data/CurrentPageReference.json' from 'wireCPR.test.js'
  • 後でこのエラーを修正します。
  • 10 行目は、インポートした CurrentPageReference を設定した registerTestWireAdapter を使用しています。この設定により、この後のテストでデータをモックできます。
  • 32 行目で、emit() を使用してモックデータを入力します。
  • 34 行目以降は、モックデータが preElement に更新されることを想定する Promise です。

では、テストデータファイルを作成して、テストに合格するようコードを更新しましょう。まず、__tests__ ディレクトリにモックデータファイルを保存するディレクトリを作成します。

  1. __tests__ ディレクトリを右クリックし、[New Folder (新しいフォルダ)] を選択します。
  2. 新しいディレクトリの名前に「data」と入力します。
  3. Enter キーを押します。
  4. data ディレクトリを右クリックし、[New File (新しいファイル)] を選択します。
  5. 「CurrentPageReference.json」と入力します。
  6. Enter キーを押します。
  7. 新しいファイルに次の json コードブロックを入力します。
    {
      "type": "standard__navItemPage",
      "attributes": {
        "apiName": "Wire"
      },
      "state": {}
    }
  8. ファイルを保存して、テストを実行します。
  9. テストで次のエラーメッセージを取得します。
    expect(received).not.toBeNull()
      Received: null
    できました! テストに失敗することで、コードを順に確認しながら問題をいち早く特定して処理を促進できます。

次は、HTML コードと JavaScript コードを追加します。

  1. wireCPR.html を開きます。
  2. 次のコードを template タグの内側に追加します。
      <lightning-card title="Wire CurrentPageReference" icon-name="custom:custom67">
        <pre>{currentPageRef}</pre>
      </lightning-card>
  3. ファイルを保存します。
  4. wireCPR.js を開いて、コードを次のコードに置換します。
    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. ファイルを保存して、テストを実行します。
  6. このテストに合格します。

何が起こなわれたのかを確認しましょう。@wire アダプタが使用されている場合、サービスから返された情報が検索されます。そのデータのモックを作成し、実際にサービスをコールしてデータを取得する代わりにモックを使用する必要があります。モックがあれば、現時点で存在する項目のみがテストされ、範囲外の事項はテストされません。また、テストが迅速に実行されるようになります。

Lightning データサービスワイヤアダプタの使用

次に、Lightning データサービス (LDS) に @wire を使用します。LDS を使用すると、カスタムオブジェクトや標準オブジェクトにすばやくアクセスできます。

  1. Visual Studio Code で新しい Lightning Web コンポーネントを作成します。
  2. 「wireLDS」という名前を設定します。
  3. __tests__ ディレクトリを作成します。
  4. wireLDS.test.js という名前のテストファイルを追加します。
  5. テストファイルに次のコードを追加します。
    import { createElement } from 'lwc';
    import WireLDS from 'c/wireLDS';
    import { getRecord } from 'lightning/uiRecordApi';
    import { registerLdsTestWireAdapter } from '@salesforce/sfdx-lwc-jest';
      
    // Mock realistic data
    const mockGetRecord = require('./data/getRecord.json');
      
    // Register as an LDS wire adapter
    const getRecordAdapter = registerLdsTestWireAdapter(getRecord);
      
    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
          getRecordAdapter.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
            );
          });
        });
      });
    });
  6. ファイルを保存して、テストを実行します。
  7. モックデータがないため、テストに失敗します (この後でモックデータを作成します)。

その前に、何が行われたのかをテストコードで確認しましょう。

  • 3 ~ 4 行目は、getRecordregisterLdsTestWireAdapter の新しいインポートです。
    getRecord は LDS API から取得されます。
  • 7 行目で再び data ディレクトリの getRecord.json ファイルからデータをモックしています。
  • 10 行目で LDS ワイヤアダプタに getRecord を登録し、モックデータを使用する準備を整えます。実際に使用するのは 27 行目です。
  • 27 行目で、mockGetRecord を引数として設定した getRecordAdapter で emit メソッドを使用します。
  • 29 行目以降は Promise 戻り値で、ここではさまざまな要素がモックデータを使用して更新されていることを確認します。

次に、テストに合格するためにモックデータファイルと残りのファイルを作成します。ファイルを作成するたびにテストを実行し、合格するまでテストエラーの解消状況を確認します。

  1. __tests__ ディレクトリに data ディレクトリを作成します。
  2. getRecord.json という名前のテストデータファイルを作成します。
  3. 次のコードを追加します。
    {
      "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. ファイルを保存して、テストを実行します。
  5. テストに失敗します。
  6. wireLDS.html を開き、テンプレートタグの内側に次のコードを入力します。
      <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. ファイルを保存して、テストを実行します。
  8. 今回もテストに失敗しますが、合格まであと一息です。データを取得する JavaScript コントローラを追加するだけです。
  9. wireLDS.js を開き、そのすべてのコードを次のコードで上書きします。
    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. ファイルを保存して、テストを実行します。
  11. このテストに合格します。
メモ

メモ

Lightning Web コンポーネントは、ユーザインターフェース API でサポートされているすべてのカスタムオブジェクトと標準オブジェクトから Salesforce データやメタデータにアクセスします。外部オブジェクトはサポートされていません。 

ユーザインターフェース API にアクセスする REST クライアントを使用してデータのスナップショットを取得するという方法で、テストデータを取得します。この方法のほうが JSON を手動で記述するより正確です。以下は、上記のデータを取得する REST コールの一例です (自身のアカウント ID が必要です)。

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

ところで、データの取得中にエラーが発生した場合はどうなるのでしょうか? この点もテストできます。では、wireLDS.test.js テストファイルに新しい describe ブロックを追加しましょう。 

  1. 'getRecord @wire data’ describe ブロックの直後に次のコードを追加して、'c-wire-l-d-s’ describe ブロック内に配置されるようにします。テストを明確にするために、describe ブロックをネストすることもできます。
      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
          getRecordAdapter.error();
      
          return Promise.resolve().then(() => {
            const errorElement = element.shadowRoot.querySelector('p');
            expect(errorElement).not.toBeNull();
            expect(errorElement.textContent).toBe('No account found.');
          });
        });
      });
  2. ファイルを保存して、テストを実行します。
  3. getRecordAdaptererror() メソッドを使用しているため、テストに合格します。この結果、モックデータにエラーが生じるため、account.error が true になります。

Apex ワイヤアダプタの使用

Apex の説明に進み、@wire を使用してテストするにはどうすればよいかを確認します。

LWC がインポートする Apex クラスは外部接続と見なされるため、モックする必要があります。つまり、Apex クラスを作成しなくてもテストできるということです。ここですべきことは、Apex コールからの予想される応答をモックするだけです。 

では、Apex コールを使用する LWC を作成しましょう。

  1. Visual Studio Code で新しい Lightning Web コンポーネントを作成します。
  2. 「wireApex」という名前を設定します。
  3. __tests__ ディレクトリを作成します。
  4. wireApex.test.js という名前のテストファイルを追加します。
  5. テストファイルに次のコードを追加します。
    import { createElement } from 'lwc';
    import WireApex from 'c/wireApex';
    import { registerApexTestWireAdapter } from '@salesforce/sfdx-lwc-jest';
    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');
      
    // Register as Apex wire adapter. Some tests verify that provisioned values trigger desired behavior.
    const getAccountListAdapter = registerApexTestWireAdapter(getAccountList);
      
    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
          getAccountListAdapter.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
          getAccountListAdapter.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
          getAccountListAdapter.error();
      
          return Promise.resolve().then(() => {
            const errorElement = element.shadowRoot.querySelector('p');
            expect(errorElement).not.toBeNull();
            expect(errorElement.textContent).toBe('No accounts found.');
          });
        });
      });
    });
  6. ファイルを保存して、テストを実行します。
  7. モックデータが見つからないというエラーが表示されます。

このコードの大部分は以前と同じです。このクリーンアップコードには、テストの前後にモックをリセットする jest.clearAllMocks() という新しい項目があります。2 種類のテストに 2 つのモックファイルがあるため、この項目が必要になります。1 つ目のテストは、6 つの取引先を提示する Apex コールを探します。2 つ目のテストは、取引先が見つからない場合にどうなるかをアサートします。最後のテストは、Apex でエラーが発生した場合にどうなるかをアサートします。

では、モックデータファイルと残りのコードを追加しましょう。

  1. __tests__ ディレクトリに data ディレクトリを作成します。
  2. 新しい data ディレクトリに getAccountList.jsongetAccountListNoRecords.json という名前の 2 つのファイルを作成します。
  3. 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. getAccountListNoRecords.json ファイルに次の空白の JSON オブジェクトが挿入されます。
    []
  5. 次に、wireApex.htmltemplate タグの内側に次のコードを入力します。
      <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. 最後に wireApex.js のコードを次のコードに置換します。
    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;
        }
      }
    }
    ここでは、AccountController Apex クラスから getAccountList メソッドを取得しているに過ぎません。LWC でこのメソッドを機能させるためには、@AuraEnabled(cacheable=true) アノテーションを付加する必要があります。@wire がこのアノテーションを使用して、返された error または data を関数に入力します。
  7. すべてのファイルを保存して、テストを実行します。
  8. このテストに合格します。

次の単元では、他のコンポーネントのモックに挑戦し、Jest を使用して Lightning Web コンポーネントをテストするあらゆる方法を習得します。

リソース