ワイヤーサービスの 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 の現在のページへの参照を取得し、そのテストを作成します。
- Visual Studio Code で、
lwc
フォルダーを右クリックし、[SFDX: Create Lightning Web Component (SFDX: Lightning Web コンポーネントの作成)] を選択します。 - 新しいコンポーネントの名前に
「wireCPR」
と入力します。 - Enter キーを押します。
- Enter キーを押して、デフォルトの
force-app/main/default/lwc
を受け入れます。 - 新しい
wireCPR/__tests__
フォルダーで、wireCPR.test.js
ファイルを開きます。 - 新しいファイルを次のコードで上書きします。
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) ); }); }); });
- ファイルを保存して、テストを実行します。
詳しく見てみましょう。
- 3 行目は、
CurrentPageReference
の新しいインポートです。 - 6 行目は、
PageReference
というモックデータを記載したファイルを取得します。このファイルはまだ作成していないため、テストでエラーが発生する 1 つ目の理由になります。Test suite failed to run Cannot find module './data/CurrentPageReference.json' from 'wireCPR.test.js'
後でこのエラーを修正します。 - 26 行目で、
emit()
を使用してモックデータを入力します。 - 28 行目以降は、モックデータが
preElement
に更新されることを想定する Promise です。
では、テストデータファイルを作成して、テストに合格するようコードを更新しましょう。まず、__tests__
ディレクトリにモックデータファイルを保存するディレクトリを作成します。
__tests__
ディレクトリを右クリックし、[New Folder (新しいフォルダー)] を選択します。- 新しいディレクトリの名前に
「data」
と入力します。 - Enter キーを押します。
data
ディレクトリを右クリックし、[New File (新しいファイル)] を選択します。「CurrentPageReference.json」
と入力します。- Enter キーを押します。
- 新しいファイルに次の json コードブロックを入力します。
{ "type": "standard__navItemPage", "attributes": { "apiName": "Wire" }, "state": {} }
- ファイルを保存して、テストを実行します。
- テストで次のエラーメッセージを取得します。
expect(received).not.toBeNull() Received: null
できました! テストに失敗することで、コードを順に確認しながら問題をいち早く特定して処理を促進できます。
次は、HTML コードと JavaScript コードを追加します。
wireCPR.html
を開きます。- 次のコードを
template
タグの内側に追加します。<lightning-card title="Wire CurrentPageReference" icon-name="custom:custom67"> <pre>{currentPageRef}</pre> </lightning-card>
- ファイルを保存します。
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) : ''; } }
- ファイルを保存して、テストを実行します。
- このテストに合格します。
何が行われたのかを確認しましょう。@wire
アダプターが使用されている場合、サービスから返された情報が検索されます。そのデータのモックを作成し、実際にサービスをコールしてデータを取得する代わりにモックを使用する必要があります。モックがあれば、現時点で存在する項目のみがテストされ、範囲外の事項はテストされません。また、テストが迅速に実行されるようになります。
Lightning データサービスワイヤーアダプターの使用
次に、Lightning データサービス (LDS) に @wire
を使用します。LDS を使用すると、カスタムオブジェクトや標準オブジェクトにすばやくアクセスできます。コンポーネントは、LDS を使用して Salesforce からデータを取得し、表示します。LDS アダプターを使用してデータが期待どおりに表示されることを検証するテストを作成します。
- Visual Studio Code で新しい Lightning Web コンポーネントを作成します。
「wireLDS」
という名前を設定します。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 ); }); }); }); });
- ファイルを保存して、テストを実行します。
- モックデータがないため、テストに失敗します (この後でモックデータを作成します)。
その前に、何が行われたのかをテストコードで確認しましょう。
- 3 行目は、
getRecord
の新しいインポートです。getRecord
は LDS API から取得されます。 - 6 行目で再び
data
ディレクトリのgetRecord.json
ファイルからデータをモックしています。 - 23 行目で、
mockGetRecord
を引数として設定したgetRecord
で emit メソッドを使用します。 - 25 行目以降は
Promise
戻り値で、ここではさまざまな要素がモックデータを使用して更新されていることを確認します。
次に、テストに合格するためにモックデータファイルと残りのファイルを作成します。ファイルを作成するたびにテストを実行し、合格するまでテストエラーの解消状況を確認します。
__tests__
ディレクトリにdata
ディレクトリを作成します。getRecord.json
という名前のテストデータファイルを作成します。- 次のコードを追加します。
{ "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" }
- ファイルを保存して、テストを実行します。
- テストに失敗します。
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>
- ファイルを保存して、テストを実行します。
- 今回もテストに失敗しますが、合格まであと一息です。データを取得する JavaScript コントローラーを追加するだけです。
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); } }
- ファイルを保存して、テストを実行します。
- このテストに合格します。
ところで、データの取得中にエラーが発生した場合はどうなるのでしょうか? この点もテストできます。では、wireLDS.test.js
テストファイルに新しい describe ブロックを追加しましょう。
- '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 getRecord.error(); return Promise.resolve().then(() => { const errorElement = element.shadowRoot.querySelector('p'); expect(errorElement).not.toBeNull(); expect(errorElement.textContent).toBe('No account found.'); }); }); });
- ファイルを保存して、テストを実行します。
getRecordAdapter
でerror()
メソッドを使用しているため、テストに合格します。この結果、モックデータにエラーが生じるため、account.error
が true になります。
Apex ワイヤーアダプターの使用
Apex の説明に進み、@wire
を使用してテストするにはどうすればよいかを確認します。
LWC がインポートする Apex クラスは外部接続と見なされるため、モックする必要があります。つまり、Apex クラスを作成しなくてもテストできるということです。ここですべきことは、Apex コールからの予想される応答をモックするだけです。この場合、期待されるのは Apex クラスから返される取引先が表示されることです。取引先が返された場合にはその取引先が表示され、返されない場合はメッセージが表示されることを期待するテストを作成します。
では、Apex コールを使用する LWC を作成しましょう。
- Visual Studio Code で新しい Lightning Web コンポーネントを作成します。
「wireApex」
という名前を設定します。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.'); }); }); }); });
- ファイルを保存して、テストを実行します。
- モックデータが見つからないというエラーが表示されます。
このコードの大部分は以前と同じです。このクリーンアップコードには、テストの前後にモックをリセットする jest.clearAllMocks()
という新しい項目があります。2 種類のテストに 2 つのモックファイルがあるため、この項目が必要になります。1 つ目のテストは、6 つの取引先を提示する Apex コールを探します。2 つ目のテストは、取引先が見つからない場合にどうなるかをアサートします。最後のテストは、Apex でエラーが発生した場合にどうなるかをアサートします。
では、モックデータファイルと残りのコードを追加しましょう。
__tests__
ディレクトリにdata
ディレクトリを作成します。- 新しい
data
ディレクトリにgetAccountList.json
とgetAccountListNoRecords.json
という名前の 2 つのファイルを作成します。 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" } ]
getAccountListNoRecords.json
ファイルに次の空白の JSON オブジェクトが挿入されます。[]
- 次に、
wireApex.html
のtemplate
タグの内側に次のコードを入力します。<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>
- 最後に
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
を関数に入力します。 - すべてのファイルを保存して、テストを実行します。
- このテストに合格します。
次の単元では、他のコンポーネントのモックに挑戦し、Jest を使用して Lightning Web コンポーネントをテストするあらゆる方法を習得します。
リソース
- 開発者ガイド: Lightning Web コンポーネント: ワイヤーサービスを使用したデータの取得
- 開発者ガイド: Lightning Web コンポーネント: lightning/ui*Api ワイヤーアダプターと関数
- 開発者ガイド: ワイヤーサービスを使用する Lightning Web コンポーネントの Jest テストの記述
- User Interface API Developer Guide (ユーザーインターフェース API 開発者ガイド): Get a Record (レコードの取得)
- Visual Studio Code 向け Salesforce 拡張機能: Lightning Web コンポーネント: テスト
- GitHub: salesforce/wire-service-jest-util
- GitHub: trailheadapps/lwc-recipes
- GitHub: wire-service-jest-util/docs/Migrating from version 2.x to 3.x