LWC テストケースの書き方(ハンズオン用)

2022/05/11に開催した[ゆるっとSalesforceトーク #5 LWCテストハンズオン](https://co-meeting.connpass.com/event/246409/)に関係して私自身が講師したハンズオン用の資料を公開します。

なお対象読者として、ある程度Salesforceの知識が多少ある人（組織のSalesforce CLIコマンドを知っている人、DevHub組織という存在を知っている人、LWCを知ってる人、画面フローを触った事がある人）を想定しております。当時は言葉で解説しながら進めたため、今回のQiitaにアップする際に加筆しました。もし言葉が足りないところはご了承ください。


## 事前準備

### その1: 各種ツールをインストールしておく

当ハンズオンをする前に、お使いのPCに、*Node.js（v14 以上）*、*Salesforce CLI*、*JDK*、*VSCode*、*VS Code 向け Salesforce 拡張機能*をインストールしておく必要があります。

詳細は以下「VS Code 向け Salesforce 拡張機能のインストール」を参照してください。
<Salesforce Developers Japan | VS Code 向け Salesforce 拡張機能のインストール>
https://developer.salesforce.com/tools/vscode/ja/getting-started/install
※VSCodeのターミナル上で以下コマンドを実行できる環境構築の必要があります。

```
node --version
sfdx --version
```

### その2: Dev Hub が有効になったSalesforce組織を用意しておく

開発用として自由に利用できるSalesforce組織を保持していない場合は、http://developer.salesforce.com/signup  にアクセスし、Developer Edition アカウントのサインアップの説明に従ってください。Developer Edition アカウントによって、自由にアクセスできるDeveloper Edition 組織を作成することができます。

Trailheadを活用している人は、Playgroundでも同様に利用可能です。Playgroundを利用する場合は、事前にパスワードをリセットして、ユーザーIDとログインパスワードを把握するように設定してください。

Salesforce 組織のDev Hubを有効にするには以下の手順に従い有効化してください。

1. 作成したSalesforce 組織 にシステム管理者としてログインします。
2. [設定] から、[クイック検索] ボックスに「Dev Hub」と入力し、*[Dev Hub]* を選択します。
3. Dev Hub を有効にするには、*[有効化]* をクリックします。
    Dev Hub は一度有効化すると、無効化できません。


## LWCテストの基本の流れ

まずは簡単なLWCとLWCテストコードを作成して、デバッグする方法を紹介します。

### 【基本の流れ】-1.プロジェクトを作成

VSCodeのコマンドパレットを開き(Ctrl+Shift+P/Cmd+Shift+P)、`[SFDX: プロジェクトを作成 (Create Project) ]-[標準]`を選択もしくは、以下のコマンドを実行してSalesforce DX プロジェクトを作成します。

プロジェクト名：`lwc-test-hands-on`

`sfdx force:project:create --projectname lwc-test-hands-on --defaultpackagedir sfdx-src`

:::note warn
ソースコードをのフォルダを`sfdx-src`と指定してプロジェクトを作成してしまっているため、コマンドパレットを使ってプロジェクト作成した人は、以後`sfdx-src`を`force-app`に置き換えて読んでください。
:::


### 【基本の流れ】-2.sfdx-lwc-jest のインストール

LWCテスト用の環境をこちらのコマンドを実行してセットアップします。

`sfdx force:lightning:lwc:test:setup`


### 【基本の流れ】-3.LWCコンポーネント作成

・VSCodeで「**SFDX: Lightning Web コンポーネントを作成**」もしくは以下コマンドで“`hello`”作成する

`sfdx force:lightning:component:create -n hello -d sfdx-src/main/default/lwc --type lwc`

```sfdx-src/main/default/lwc/hello/hello.html
<template>
  <span class="welcome-message">
    Hello, {name}!
  </span>
</template>
```

```sfdx-src/main/default/lwc/hello/hello.js
import { LightningElement, api } from 'lwc';

export default class Hello extends LightningElement {
  @api name;
}
```


### 【基本の流れ】-4.LWCコンポーネント用のテストコード作成

VSCodeで「**SFDX: Create Lightning Web Component Test**」実行もしくは、以下コマンドでLWCテストコード作成します。

`sfdx force:lightning:lwc:test:create -f sfdx-src/main/default/lwc/hello/hello.js`

```sfdx-src/main/default/lwc/hello/\_\_tests\_\_/hello.test.js

import { createElement } from 'lwc';
import Hello from 'c/hello';

describe('c-hello', () => {
    afterEach(() => {
        // The jsdom instance is shared across test cases in a single file so reset the DOM
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('welcomeメッセージが表示されることを確認', () => {
        const element = createElement('c-hello', {
            is: Hello
        });
        element.name = 'Hanamizuki';
        document.body.appendChild(element);
        // 展開された情報が指定引数をもとに展開されているか確認する
        const spanEl = element.shadowRoot.querySelector('span.welcome-message');
        expect(spanEl.innerHTML).toBe('Hello, Hanamizuki!');
    });
});
```

### 【基本の流れ】-5.LWCテスト

`npm run test:unit`
もしくはVSCodeの機能を活用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/9eec4a92-05c6-2378-1cfc-dedfd70aa4d4.png)

### 【基本の流れ】-6.LWC デバッグとブレークポイント

正しく拡張機能がインストールされていると、VSCodeのソースコード上からデバッグ実行できます。
以下のようなに、ブレークポイントを行番号前に指定して、デバッグ実行するとステップ実行できるようになったり、変数の中身をのぞくことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/14d9d039-cb83-cb3c-d9d7-2eb1c723fd9a.png)

### 【基本の流れ】 まとめ

VSCode上だけでユニットテストケースが動作することを確認できましたね。

実際の開発では画面に描画しながら動作検証するのが必要です。今回流れをみて、Salesforce組織がなくてもテストができるということを理解してもらえたと思います。よって、ユニットテストケースの動作確認だけならSalesforce組織が不要なので、軽い修正程度ならデプロイせずに検証できるし、、GitHub Actionを利用して、Push時にテストを動かすように環境を整えたりできそうですね。


## 【本題】wireを使ったLWCテストケースはどう書くのか?

メインコンテンツです。
上記で紹介したようなシンプルなLWCコードは、正直使うことがありません。

実際の開発では、サーバーに必要なデータを問い合わせして取得するような仕組みが多いです。
ここでは、LWCのwireサービスを使った例をサンプルにLWCテストコードの書き方をハンズオンを通して体験していきましょう。

### 【事前準備】サンプルとなるLWC(MySimpleTable)を作成

今回はサンプルとして、lightning-datatableを活用し指定されたレコード一覧を表示する画面フロー用のコンポーネントを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/57640233-e80d-c496-1c98-979b163b5477.png)

このLWCの実装ポイントとしては以下のとおりです。
　・ INPUT値として指定されたレコード一覧を表形式で表示する
　・ 表の列名を問い合わせする
　　・ `lightning/uiObjectInfoApi`が提供しているwireアダプター`getObjectInfo`を活用しオブジェクト情報を取得。
　　・ 取得したオブジェクト情報から「各項目のラベル」を表の列名とする
　　・　`getObjectInfo`の利用にはオブジェクト名が必要。あえて誤ったオブジェクト名を指定した場合はエラーとする。



では、作成していきましょう。


VSCodeで「`SFDX: Lightning Web コンポーネントを作成`」もしくは以下コマンドで"`mySimpleTable`"作成します。

`sfdx force:lightning:component:create -n mySimpleTable -d sfdx-src/main/default/lwc --type lwc`

以下のように内容をコピペしてください。

```sfdx-src/main/default/lwc/mySimpleTable/mySimpleTable.html
<template>
  <template if:false={errorInfo}>
    <!-- LWC実行時にエラーが発生しない場合-->

    <!-- サーバー問い合わせ時に表示する内容-->
    <div if:true={isLoading} class="spinnerHolder">
      <lightning-spinner alternative-text="Loading" size="small"></lightning-spinner>
    </div>
    <!-- 表形式でデータ表示-->
    <div if:true={hasRecords} style={style}>
      <lightning-datatable
              key-field="id"
              data={records}
              columns={columns}
              onrowselection={handleSelectedRow}>
      </lightning-datatable>
    </div>
    <!-- データが存在しない時の表示-->
    <template if:false={hasRecords}>
      <div class="slds-m-around_x-small">データはありません。</div>
    </template>
  </template>
  <template if:true={errorInfo}>
    <!-- サーバー問い合わせ時にエラー発生した時に表示する内容-->
    <div
      class="slds-scoped-notification slds-media slds-media_center slds-theme_error"
      role="status"
    >
      <div class="slds-media__figure">
        <span class="slds-icon_container slds-icon-utility-error" title="error">
          <lightning-icon
            icon-name="utility:error"
            title="Error"
            size="x-small"
            variant="inverse"
          ></lightning-icon>
        </span>
      </div>
      <div class="slds-media__body">
        <p>【MySimpleTable Component Error】An error occurred when retrieving the data.</p>
        <ul class="slds-p-left_medium slds-list_dotted">
          <li>{errorInfo.message}</li>
          <li if:true={errorInfo.method}>
            error method: {errorInfo.method}
            <span if:true={errorInfo.arguments}> ({errorInfo.arguments}) </span>
          </li>
        </ul>
      </div>
    </div>
  </template>
</template>
```

```sfdx-src/main/default/lwc/mySimpleTable/mySimpleTable.css
/* 自動生成では作成されないファイルです。ファイル作成してください */
.spinnerHolder {
  position: relative;
  height: 80px;
}
```

```sfdx-src/main/default/lwc/mySimpleTable/mySimpleTable.js
import { LightningElement, api, wire } from 'lwc';
import { FlowAttributeChangeEvent } from 'lightning/flowSupport';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';

export default class MySimpleTable extends LightningElement {
  @api records;
  @api height;
  @api objectName;
  @api selectedRecords;

  columns;
  wiredObjectInfo;
  errorInfo = null;

  // データの有無返却
  get hasRecords() {
    return (this.records && this.records.length > 0);
  }

  // 表形式の列名となる項目一覧を取得
  get displayFieldNames() {
    // 引き渡されたレコードに存在する項目を利用する
    return (this.hasRecords) ? Object.keys(this.records[0]) : [];
  }

  // 表の高さCSSスタイルを返却
  get style() {
    if (!this.height || this.height === 0) return ''; // 最低限表示する
    return 'height: ' + this.height + 'px';
  }

  // サーバー問い合わせ中であるか返却
  get isLoading() {
    if (this.errorInfo) return false; //  エラー発生中ではない
    return !(this.wiredObjectInfo && this.wiredObjectInfo.data);
  }

  // 項目情報を返却
  get fieldInfos() {
    return this.wiredObjectInfo?.data?.fields;
  }

  // wireサービスを利用して指定オブジェクトの定義情報を取得する
  @wire(getObjectInfo, { objectApiName: '$objectName' })
  wiredObjectInfoCallback(value) {
    this.wiredObjectInfo = value;
    const { data, error } = value;
    if (data) {
      this.errorInfo = null;
      this._initializationColumns();
    } else if (error) {
      console.error(error);
      this.errorInfo = {
        message: error.body.statusCode + ':' + error.body.errorCode + ':' + error.body.message,
        method: 'getObjectInfo',
        arguments: 'objectApiName=' + this.objectName
      };
    }
  }

  // 表形式の列情報を初期化
  _initializationColumns() {
    this.columns = this.displayFieldNames
      .filter((apiName) => !['Id', 'Name'].includes(apiName))
      .map((apiName) => {
        const column = {
          fieldName: apiName,
          label: this.fieldInfos[apiName].label,
          type: 'text'
        };
        return column;
      });

    if (this.displayFieldNames.includes('Name')) {
      // 先頭にNameを表示する
      this.columns.unshift({
        fieldName: 'Name',
        label: this.fieldInfos.Name.label,
        type: 'text'
      });
    } else {
      // Nameが存在しない場合はIdを表示する
      this.columns.unshift({
        fieldName: 'Id',
        label: this.fieldInfos.Id.label,
        type: 'text'
      });
    }
  }

  // 選択された行の情報をselectedRecordsにセット
  handleSelectedRow(event) {
    const selectedRows = event.detail.selectedRows;
    this.dispatchEvent(new FlowAttributeChangeEvent('selectedRecords', selectedRows));
  }
}
```

```sfdx-src/main/default/lwc/mySimpleTable/mySimpleTable.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>54.0</apiVersion>
    <!-- コンポーネント一覧にひょうじされるようにisExposedをtrueに変更する -->
    <isExposed>true</isExposed>
    <masterLabel>MySimpleTable</masterLabel>
    <description>レコード一覧表示</description>
    <!-- 表示される対象は画面フローのみとする -->
    <targets>
      <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
      <!-- 画面フローのプロパティ定義 -->
      <targetConfig targets="lightning__FlowScreen">
        <propertyType name="T" extends="SObject" label="オブジェクト" />
        <property role="inputOnly" label="オブジェクトAPI参照名" name="objectName" type="String" />
        <property role="inputOnly" label="レコード一覧" name="records"  type="{T[]}" />
        <property role="inputOnly" label="高さ(px)" name="height" type="Integer" />
        <property role="outputOnly" label="選択されたレコード一覧" name="selectedRecords" type="{T[]}" />
      </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
```

色々書いてますね。
テストコードを合わせて書かないと不安になるようなコードですね。

### 【脱線】スクラッチ組織を用意し、作成したLWCをプッシュして画面フローで見てみよう

テストコードを書く前に、今回作成した物がどんなものか動かして見てみたいと思いませんか？
何を作ったか理解するためにも、作成したLWCコンポーネントを実際に動かしてみましょう。

**DevHub認証**

スクラッチ組織を作成するためにDevHub認証をします。

`sfdx force:auth:web:login --setdefaultdevhubusername`

```
// 認証したDevHubの制限を確認
sfdx force:limits:api:display -u <DevHubユーザーID>
// 作成しているスクラッチ組織の確認
sfdx force:org:list
```

:::note info
もしも作成可能なスクラッチ組織数が、制限を達成し、枯渇しているようならば、DevHub組織にログインして、有効なスクラッチ組織の中で利用していないやつを削除(`sfdx force:org:delete -u <USERNAME>`)してください。もしくは別の利用可能なDevHub組織にログインし直してください。
:::

#### 【脱線の1】スクラッチ組織作成
以下コマンドを実行してスクラッチ組織を作成します。
サンプルデータが欲しいので、コマンド実行する前に`project-scratch-def.json`ファイルを開き、`"hasSampleData": true,`を追加します。(スクラッチ組織定義ファイルの詳細は[こちら](https://developer.salesforce.com/docs/atlas.ja-jp.sfdx_dev.meta/sfdx_dev/sfdx_dev_scratch_orgs_def_file.htm)を参照してください。)

■スクラッチ組織作成コマンド
`sfdx force:org:create -f ./config/project-scratch-def.json -s -a lwc-test-hands-on-SCRATCH -d 30 -w 20 `

以下コマンドで作成したスクラッチ組織をブラウザで開いて確認できます。

`sfdx force:org:open -u lwc-test-hands-on-SCRATCH`

#### 【脱線の2】スクラッチ組織へソースコードをデプロイ

`sfdx force:source:push -u lwc-test-hands-on-SCRATCH`


#### 【脱線の3】フローを作成して今作ったLWCの動きを見てみよう

取引先レコード一覧を取得して、取引先名と電話番号をテーブル表示、選択できる画面フローを作成します。
サンプルデータ有りでスクラッチ組織を作りましたが、好みに応じて、事前に適当な取引先を登録しておくのもありです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/e04a7903-5bda-9073-b17e-603307eff504.png)


##### 【脱線の3】-1.フロー完成形

ここで作成するフローは以下のとおりです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/65e5ab3e-0aff-f0c6-21b4-dd7f56e249fe.png)

スクラッチ組織を開き、[設定] から、[クイック検索] ボックスに「フロー」と入力し、*[フロー]* を選択します。
[新規フロー]ボタンを押して、[画面フロー]を選択し、[作成]ボタンをクリックします。
実際に作成してみましょう。


##### 【脱線の3】-2. `getAccounts`
フロー内の[＋]ボタンをおして、要素「レコードを取得」を追加して以下のように設定してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/ff46bdbd-3c81-aefb-08de-7bb5ccd43388.png)

意識するポイントは、最後のデータの保存方法です。
ここでは表形式で表示したい項目のみを指定します。
ここではNameとPhoneのみ保存するように項目指定しております。

##### 【脱線の3】-3. `MySimpleTable表示`,  API参照名:`screen1`
フロー内の[＋]ボタンをおして、要素「画面」を追加して以下のように設定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/a75d5ef5-f385-c6da-85f0-4c074650b1cf.png)


 - サンプル例1:正しいケース
   - カスタムコンポーネントMySimpleTable1つ目-API参照名:`displayTable`
   - カスタムコンポーネントMySimpleTable1つ目-オブジェクト: `取引先`
   - カスタムコンポーネントMySimpleTable1つ目-オブジェクトAPI 参照名: `Account`
   - カスタムコンポーネントMySimpleTable1つ目-レコード一覧: `{!getAccounts}`
   - カスタムコンポーネントMySimpleTable1つ目-高さ(px): `200`

 - サンプル例2:異常ケース(意図的にデータを指定しない。存在しないオブジェクトAPI 参照名を指定したケース）
   - カスタムコンポーネントMySimpleTable2つ目-API参照名:`displayTableError`
   - カスタムコンポーネントMySimpleTable2つ目-オブジェクト: `取引先`
   - カスタムコンポーネントMySimpleTable2つ目-オブジェクトAPI 参照名: `aaaaa`
   - カスタムコンポーネントMySimpleTable2つ目-レコード一覧: `未入力`
   - カスタムコンポーネントMySimpleTable2つ目-高さ(px): `未入力`

##### 【脱線の3】-4. `選択した要素を表示`,  API参照名:`screen2`
フロー内の[＋]ボタンをおして、要素「画面」を追加して以下のように設定してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/d9415294-be7f-f53a-f7f5-92139c1464d0.png)

 - 表示テキスト-API参照名: `displaySelectedRecords`
 - 表示テキスト-内容:`{!displayTable.selectedRecords}`


##### 【脱線の3】-5.実行結果

右上メニューの[保存]をクリックして、以下のようにフローの表示ラベル、API参照名を入力して保存します。

 - フローの表示ラベル:`MySimpleTableサンプルFlow`
 - フローの API 参照名:`MySimpleTableSample_Flow`

その後、右上メニューの[デバッグ]をクリックしてデバッグ実行してみましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/266aef63-8dae-f185-acbe-51bb0cef7ba6.png)


### 【本題】 wireを使ったLWCテストケースを作成する

さてさて　遅くなりましたが此処からが本題です。今回作成したLWCの動きを確認できました。
ここで、すべてのケースを網羅するようにコンポーネントの設定を指定していくのは可能ですが人の手がかかってしまい大変です。
自動化できるようにもテストコードを書いて対応できるようにしてみましょう。

VSCodeで「**SFDX: Create Lightning Web Component Test**」実行もしくは、以下コマンド実行でLWCテストケースファイルを自動生成します。

`sfdx force:lightning:lwc:test:create -f sfdx-src/main/default/lwc/mySimpleTable/mySimpleTable.js`

中身を開き以下のコードをコピペしてください。

```sfdx-src/main/default/lwc/mySimpleTable/\_\_tests\_\_/mySimpleTable.test.js

import { createElement } from 'lwc';
import MySimpleTable from 'c/mySimpleTable';
import { getObjectInfo } from 'lightning/uiObjectInfoApi';

// Error
const mockGetObjectInfoError = require('./data/getObjectInfo_Error.json');

// object: Account
// type:Name, phone, number, address, picklist, url, date
const mockGetObjectInfoByAccount = require('./data/Account/getObjectInfo.json');
const mockApiRecordsByAccount = require('./data/Account/api_records.json');
const mockExpectedColumnsByAccount = require('./data/Account/expected_columns.json');

jest.mock(
  'lightning/flowSupport',
  () => {
    return { FlowAttributeChangeEvent: {} };
  },
  { virtual: true }
);

describe('c-my-simple-table', () => {
  afterEach(() => {
    // The jsdom instance is shared across test cases in a single file so reset the DOM
    while (document.body.firstChild) {
      document.body.removeChild(document.body.firstChild);
    }
  });

  it('テストケース1:正常系挙動', async () => {
    const element = createElement('c-my-simple-table', {
      is: MySimpleTable
    });
    element.records = mockApiRecordsByAccount;
    element.height = '200';
    element.objectName = 'Account';
    document.body.appendChild(element);

    // Emit data from @wire
    await getObjectInfo.emit(mockGetObjectInfoByAccount);

    // アサーション
    const divEl = element.shadowRoot.querySelector('div');
    // eslint-disable-next-line @lwc/lwc/no-inner-html
    expect(divEl.outerHTML).toBe(
      '<div style="height: 200px"><lightning-datatable></lightning-datatable></div>'
    );
    const dbEl = element.shadowRoot.querySelector('lightning-datatable');
    const expectedData = JSON.stringify(mockApiRecordsByAccount);
    const receivedDate = JSON.stringify(dbEl.data);
    const expectedColumns = JSON.stringify(mockExpectedColumnsByAccount);
    const receivedColumns = JSON.stringify(dbEl.columns);
    expect(dbEl.keyField).toBe('id');
    expect(receivedDate).toBe(expectedData);
    expect(receivedColumns).toBe(expectedColumns);
  });

  it('テストケース2:height未指定', async () => {
    const element = createElement('c-my-simple-table', {
      is: MySimpleTable
    });
    element.records = mockApiRecordsByAccount;
    element.height = '';
    element.objectName = 'Account';
    document.body.appendChild(element);

    // Emit data from @wire
    await getObjectInfo.emit(mockGetObjectInfoByAccount);

    // アサーション
    const divEl = element.shadowRoot.querySelector('div');
    // eslint-disable-next-line @lwc/lwc/no-inner-html
    expect(divEl.outerHTML).toBe('<div><lightning-datatable></lightning-datatable></div>');
  });

  it('テストケース3:recordsのデータなし', async () => {
    const element = createElement('c-my-simple-table', {
      is: MySimpleTable
    });
    element.records = [];
    element.height = '200';
    element.objectName = 'Account';
    document.body.appendChild(element);

    // Emit data from @wire
    await getObjectInfo.emit(mockGetObjectInfoByAccount);

    // アサーション
    const divEl = element.shadowRoot.querySelector('div');
    // eslint-disable-next-line @lwc/lwc/no-inner-html
    expect(divEl.outerHTML).toBe('<div class="slds-m-around_x-small">データはありません。</div>');
  });


  it('テストケース4:ローディング中の表示確認', async () => {
    const element = createElement('c-my-simple-table', {
      is: MySimpleTable
    });
    element.records = mockApiRecordsByAccount;
    element.height = '200';
    element.objectName = 'Account';
    document.body.appendChild(element);

    // アサーション
    const divEl = element.shadowRoot.querySelector('div');
    // eslint-disable-next-line @lwc/lwc/no-inner-html
    expect(divEl.innerHTML).toBe('<lightning-spinner></lightning-spinner>');
  });

  it('テストケース5:getObjectInfoでエラー発生ケース', async () => {
    const element = createElement('c-my-simple-table', {
      is: MySimpleTable
    });
    element.fieldNames = '';
    element.records = mockApiRecordsByAccount;
    element.height = '200';
    element.objectName = 'Account';
    document.body.appendChild(element);

    // error data from @wire
    await getObjectInfo.error(mockGetObjectInfoError);

    // アサーション
    const divEl = element.shadowRoot.querySelector('div.slds-theme_error');
    expect(divEl.className).toBe('slds-scoped-notification slds-media slds-media_center slds-theme_error');
    expect(divEl.childElementCount).toBe(2);
    expect(divEl.children[1].className).toBe('slds-media__body');
    const divBody = divEl.children[1];
    expect(divBody.childElementCount).toBe(2);
    // eslint-disable-next-line @lwc/lwc/no-inner-html
    expect(divBody.children[0].outerHTML).toBe('<p>【MySimpleTable Component Error】An error occurred when retrieving the data.</p>');
    expect(divBody.children[1].className).toBe('slds-p-left_medium slds-list_dotted');
    const errUiList = divBody.children[1];
    expect(errUiList.childElementCount).toBe(2);

    // eslint-disable-next-line @lwc/lwc/no-inner-html
    expect(errUiList.children[0].outerHTML).toBe('<li>403:INSUFFICIENT_ACCESS:このレコードへのアクセス権がありません。システム管理者にサポートを依頼するか、アクセス権を要求してください。</li>');
    // eslint-disable-next-line @lwc/lwc/no-inner-html
    expect(errUiList.children[1].outerHTML).toBe('<li>error method: getObjectInfo<span> (objectApiName=Account) </span></li>');
  });
});
```

#### Test実行前に、事前に以下フォルダとファイルを作成しよう

上記のコードをコピペしただけでは動きません。必要となるテストデータを用意します。

改めて、今回のLWCテストハンズオンに関係したソースコードは以下GitHubにて公開しております。
https://github.com/hanamizuki10/lwc-test-hands-on
（<font color="#905">こちらのハンズオンと少し違いがあり、こちらのソース上にはCaseを利用したテストケースも含まれております</font>）

テストデータはボリュームがあるので、リポジトリからコードをコピペしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/cc48afa6-7b54-dbd6-ed84-a235c4217703.png)


##### 作成するフォルダ（モックデータ格納先）

 -  sfdx-src/main/default/lwc/mySimpleTable/\_\_tests\_\_/**data**/**Account**/


##### 作成するファイル（モックデータとなるJSONファイル）
 - 格納先：sfdx-src/main/default/lwc/mySimpleTable/\_\_tests\_\_/data/
    - sfdx-src/main/default/lwc/mySimpleTable/\_\_tests\_\_/data/**getObjectInfo_Error.json**
      - `import { getObjectInfo } from 'lightning/uiObjectInfoApi';`コール異常時のレスポンス用テストデータです。
      - ソースはこちら
        - https://github.com/hanamizuki10/lwc-test-hands-on/blob/master/sfdx-src/main/default/lwc/mySimpleTable/__tests__/data/getObjectInfo_Error.json

 - 格納先：sfdx-src/main/default/lwc/mySimpleTable/\_\_tests\_\_/data/Account/
    - sfdx-src/main/default/lwc/mySimpleTable/\_\_tests\_\_/data/Account/**getObjectInfo.json**
      - `import { getObjectInfo } from 'lightning/uiObjectInfoApi';`コール正常時のレスポンス用テストデータです。
      - ソースはこちら
        - https://github.com/hanamizuki10/lwc-test-hands-on/blob/master/sfdx-src/main/default/lwc/mySimpleTable/__tests__/data/Account/getObjectInfo.json

    -  sfdx-src/main/default/lwc/mySimpleTable/\_\_tests\_\_/data/Account/**api_records.json**
       - 自作コンポーネント`MySimpleTable`の引数`records`のテストデータです。
       - ソースはこちら
         - https://github.com/hanamizuki10/lwc-test-hands-on/blob/master/sfdx-src/main/default/lwc/mySimpleTable/__tests__/data/Account/api_records.json

    - sfdx-src/main/default/lwc/mySimpleTable/\_\_tests\_\_/data/Account/**expected_columns.json**
      - 自作コンポーネント`MySimpleTable`が内部にて生成する`columns`のテストデータです。データが正しく作られたか確認用に利用します。
      - ソースはこちら
        - https://github.com/hanamizuki10/lwc-test-hands-on/blob/master/sfdx-src/main/default/lwc/mySimpleTable/__tests__/data/Account/expected_columns.json


**【補足】これらのJSONファイルは、どうやって作成したのか？**
以下のようにメソッド内にログを埋め込み、実際にコンソールに吐き出して内容を確認しました。
```diff
//sfdx-src/main/default/lwc/mySimpleTable/mySimpleTable.js  より抜粋
  @wire(getObjectInfo, { objectApiName: '$objectName' })
  wiredObjectInfoCallback(value) {
+   console.log(JSON.stringify(value));
```
### LWCテスト

以下コマンドもしくはVSCodeの機能を活用してLWCテストを実行してみましょう。
`npm run test:unit`

:::note warn
【ハンズオン中に実際に発生した事例より】
もし、LWCテスト実行時にコード「`return this.wiredObjectInfo?.data?.fields;`」にてエラーが発生したら、nodeのバージョンが低く[オプショナルチェーン](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Optional_chaining)に対応していないかもしれません。nodeのバージョンはv14以上に変更してもらえると解決します。もしくは別の書き方に見直してください。
:::


## その他.LWCテスト作成における耳寄りな情報とポイント３つ

以下ガイドは参考になるため情報共有します。

**公式開発ガイド: Lightning Web コンポーネントのテスト**
https://developer.salesforce.com/docs/component-library/documentation/ja-jp/53.0/lwc/lwc.testing

**公式Trailheadモジュール: Lightning Web コンポーネントのテスト**
https://trailhead.salesforce.com/ja/content/learn/modules/test-lightning-web-components

**trailheadapps/[lwc-recipes](https://github.com/trailheadapps/lwc-recipes)**
`git clone git@github.com:trailheadapps/lwc-recipes.git`


### _ポイント1: 基本的にlwc-recipesがすごく参考になる_

例えば以下のような記述が存在する
```sfdx-src/main/default/lwc/miscNotification/*tests*/miscNotification.test.js
        // Select combobox for simulating user input
        const comboboxEl =
            element.shadowRoot.querySelector('lightning-combobox');
        comboboxEl.value = TOAST_VARIANT;
        comboboxEl.dispatchEvent(new CustomEvent('change'));

        // Select button for simulating user interaction
        const buttonEl = element.shadowRoot.querySelector('lightning-button');
        buttonEl.click();

        // Wait for any asynchronous DOM updates
        await flushPromises();

        // Check if toast event has been fired
        expect(handler).toHaveBeenCalled();
        expect(handler.mock.calls[0][0].detail.title).toBe(TOAST_TITLE);
        expect(handler.mock.calls[0][0].detail.message).toBe(TOAST_MESSAGE);
        expect(handler.mock.calls[0][0].detail.variant).toBe(TOAST_VARIANT);
```

こちらは、LWC内のlightning-comboboxにchangeイベントが発生して、その後にlightning-buttonにclickイベントを発動した時に、想定されるToastが表示されるか確認するようなテストケースです。イベントの書き方の参考にできますね。


### _ポイント2: sfdx-lwc-jestには提供していないスタブが存在する_

sfdx-lwc-jestが提供しているモックスタブは以下のようになっています。

参考：Jest テストパターンとモックの連動関係
https://developer.salesforce.com/docs/component-library/documentation/ja-jp/53.0/lwc/lwc.unit_testing_using_jest_patterns

> [sfdx-lwc-jest リポジトリ](https://github.com/salesforce/sfdx-lwc-jest/)には Salesforce が提供するモックコンポーネントがあります。コンポーネントのソースは [lightning-stubs ディレクトリ](https://github.com/salesforce/sfdx-lwc-jest/tree/master/src/lightning-stubs)にあります。Lightning 基本コンポーネントを含む、これらのモックコンポーネントをテストに使用します。モックコンポーネントは実際のコンポーネントの API と一致しますが、すべての機能を備えているわけではなく、テスト用のリソースとして機能します。これらのモックコンポーネントによってイベントが起動されることはありませんが、それらのイベントハンドラを呼びだすことができます。

```js
// 例えばuiワイヤーサービスとして以下のような記述をLWC上に記載した時
import { getObjectInfo } from "lightning/uiObjectInfoApi";
```

sfdx-lwc-jestはスタブを提供しています。ソースコードは以下のような感じです。
https://github.com/salesforce/sfdx-lwc-jest/blob/master/src/lightning-stubs/uiObjectInfoApi/uiObjectInfoApi.js
```https://github.com/salesforce/sfdx-lwc-jest/blob/master/src/lightning-stubs/uiObjectInfoApi/uiObjectInfoApi.js
/*
 * Copyright (c) 2018, salesforce.com, inc.
 * All rights reserved.
 * SPDX-License-Identifier: MIT
 * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
 */
import { createLdsTestWireAdapter } from '@salesforce/wire-service-jest-util';

export const getObjectInfo = createLdsTestWireAdapter(jest.fn());
export const getObjectInfos = createLdsTestWireAdapter(jest.fn());
export const getPicklistValues = createLdsTestWireAdapter(jest.fn());
export const getPicklistValuesByRecordType = createLdsTestWireAdapter(jest.fn());
```

スタブが提供されているから、以下のように1行でモックデータを指定できます。
```***.test.js
    // Emit data from @wire(正常にレスポンスをもらったケース）
    await getObjectInfo.emit(mockGetObjectInfoByAccount);
```
```***.test.js
    // error data from @wire（エラーが発生したときのケース）
    await getObjectInfo.error(mockGetObjectInfoError);
```

しかし、sfdx-lwc-jestが提供していない自作Apexクラスや、標準でもスタブ提供が存在しないライブラリが存在します。
具体例は以下の通り。

```js
// 自作Apexクラスケース （当たり前だが sfdx-lwc-jest にはスタブが存在しない）
import getContactList from '@salesforce/apex/ContactController.getContactList';
```
```js
// 公式が提供しているライブラリだが sfdx-lwc-jest にスタブが存在しないケース
import { FlowAttributeChangeEvent } from "lightning/flowSupport";
import { refreshApex } from '@salesforce/apex';
```
このようなライブラリを活用したLWCはテスト実行時に以下のエラーが発生します。
`Cannot find module 'lightning/flowSupport' from 'sfdx-src...該当ソースコード.js'`

対策としては以下の通りjest.mockを使って登録します。
```***.test.js
// 自作Apexクラスケース （当たり前だが sfdx-lwc-jest にはスタブが存在しない）
// import getContactList from '@salesforce/apex/ContactController.getContactList';
jest.mock(
    '@salesforce/apex/ContactController.getContactList',
    () => {
        const {
            createApexTestWireAdapter
        } = require('@salesforce/sfdx-lwc-jest');
        return {
            default: createApexTestWireAdapter(jest.fn())
        };
    },
    { virtual: true }
);
```
```***.test.js
// 公式が提供しているライブラリだが sfdx-lwc-jest にスタブが存在しないケース
// import { FlowAttributeChangeEvent } from "lightning/flowSupport";
jest.mock(
  'lightning/flowSupport', () => {return {FlowAttributeChangeEvent: {}}},
  { virtual: true }
);
```
```***.test.js
// 公式が提供しているライブラリだが sfdx-lwc-jest にスタブが存在しないケース
//　import { refreshApex } from '@salesforce/apex';
jest.mock(
  '@salesforce/apex',
  () => {
      return {
          refreshApex: jest.fn(() => Promise.resolve())
      };
  },
  { virtual: true }
);
```

これらは、[lwc-recipes](https://github.com/trailheadapps/lwc-recipes)内のファイルに対して、「`jest.mock`」で検索すると様々な書き方を発見できますので参考にしてみてください。
上記のように登録することで`mockResolvedValue`や`mockRejectedValue`を利用してモックデータを登録できるようになります。

```***.test.js
// Assign mock value for resolved Apex promise(正常にレスポンスをもらったケース）
getContactList.mockResolvedValue(APEX_CONTACTS_SUCCESS);
```

```***.test.js
// Assign mock value for rejected Apex promise（エラーが発生したときのケース）
getContactList.mockRejectedValue(APEX_CONTACTS_ERROR);
```


### _ポイント3: @api のフィールド名を間違えても、エラーは発生しないということ_

最後に初心者ネタでもあります。
いろいろLWCの引数を見直したりといじりまくっていたある時、どうやってもシナリオ通りに動かないテストケースが発生しました。
原因は、純粋にフィールド名を間違えていただけ。
ささやかな誤字はよくあることであるが、分かりやすいエラーメッセージは発生しないので原因を見つけるのに時間がかかってしまったのです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/504068/391894dc-99bf-76ed-49d7-043029cbb609.png)

expectは情報が一致しなければ例外を発生することでお知らせする仕組みです。よって、エラー原因を探そうと「try~catch」で囲うと常に正常終了してしまうため、私は当初つまづきました。

特に初心者は気をつけるポイントです。

---

以上、１時間の中に詰め込みすぎた気はしますが、ありがとうございました。

ではでは！