Salesforce勉強しないと行けないのでせんとす。
雑多に備忘メモを書いておく場所。
とりあえず勉強用にアカウント作る
一番はじめにやってよくわかったもの
このハンズオンが一番イメージつけやすかった。
画面フローの作成やトリガーフローの作成を全部触れるので、基本的なイメージをつけやすかった。
テスティング(自動テスト)について
アプリケーションを頻繁に更新する場合、回帰テストなどの自動化しておくことで、頻繁にアプリケーションを更新してもこれまで作成したアプリケーションが壊れていないことを保証できるため、ケースによっては導入していくことが望ましい。
Salesforceのテスティングの考え方
一応提供しているテストの機能は存在するし、場合によってはテストの自動化を採用するケースは公式として言及されている。
https://trailhead.salesforce.com/ja/content/learn/modules/continuous-testing/automate-your-tests
https://trailhead.salesforce.com/ja/content/learn/modules/apex_testing
https://trailhead.salesforce.com/ja/content/learn/modules/test-lightning-web-components
https://developer.salesforce.com/jpblogs/2022/11/run-end-to-end-tests-with-the-ui-test-automation-model-utam-jp
が、実態としてはテスト自動化を運用しているケースは少ないのが現状だと思われる。
Copadoの「Salesforceテストの現状」レポートによると、Salesforceチームの84%が依然として手動テスト手法を他のテスト手法と組み合わせて使用しています。この組み合わせは、リリーススケジュールの遅延や結果の一貫性のなさにつながっています。手動テストの手法はスケーラビリティの限界に達しているため、現代のDevOps環境では自動テストが不可欠です。
つまり
やったほうがいいけど、実際はみんなやってはいない。
テスト設計について
とはいえ自動テストについて考えてみる。
Web開発におけるテストの考え方をそのまま当てはめることが是とは思わない(というか正解がわからないが、、、)ですが、一度考え方のヒントとして仮定してみる。
テストピラミッドの考え方について
テストピラミッドの考え方についてはこちら。
テスティングトロフィーという考え方もある(https://qiita.com/grhg/items/c42dbbf76ca9ae0dcadb)
つまり、
- 細かい部品のテストで細かい検証をしっかりやる
- アプリケーションが正しくふるまっているかのテストは必要最小限にする
という考え方になる。
底辺から頂点に向かって、**ユニットテスト(最多)→ 統合テスト(中程度)→ E2Eテスト(最少)**という階層構造を取ります。この構造は「高レベルになるほどテスト数を減らすべき」という原則に基づいています。
この考え方をSalesforceでの自動テストに基づいて整理すると以下のような構造になる。
| テストサイズ | Salesforce実装 | 推奨比率 |
|---|---|---|
| Small(単体) | Jestによるモック付きLWCテスト、純粋なApexロジックテスト | 70-80% |
| Medium(統合) | DML/SOQLを含むApexテスト、@wireモック付きLWCテスト | 15-20% |
| Large(E2E) | UTAM、Selenium、PlaywrightによるUIテスト | 5-10% |
踏まえると(実態運用を踏まえられていないので欠陥はありそうだが)以下の考え方の方が良さそう(という仮説)。
- サーバーサイドのロジックやフローのふるまいに対する検証でApexによるテストで対応できるケースは可能な限りApexによるテストコードによるテストを行う。
- 画面フローなどフロントエンドに関連する部分のテストはLWCを利用していない場合独立したテストを実行できないため、統合テストでコンポーネント間の連携と併せて、E2Eテスト(PlaywrightやProvar)で重要なユーザーフローに限定したテストを行う。
各Salseforceのテスト実装・機能について
以降、Salesforceで利用できそうなテスト実装・機能についてまとめる。
フローのテスト機能
フロー作成時の画面でテスト機能がある。
複数のテストケースが作成できる。
各テストケースにアサーションを設定して保存できるようだが、アサーションの種類が乏しく、ビジネス要件をテストしたいケースをおそらくほとんど満たせない気がするので、あまり出番は無さそうな印象。
あと、画面フローではこの機能を使えない。
Apexのテスト
Apexを使ったトリガーフローやデータの状態変化についての自動テストが作成できる。
例えば、以下のトリガー保存後フローのテストを考える。
- 取引先の電話番号を変更した場合、そこに所属する取引先責任者の電話番号も同じものに更新する。
このふるまいに対するApexのテストはこちら。
@isTest
private class AccountContactPhoneSyncTest {
/**
* Account更新時に関連するContactの電話番号がAccountの電話番号に同期されることを確認
*/
@isTest
static void testAccountPhoneUpdate_SyncsToContacts() {
// テストデータ作成
Account testAccount = new Account(
Name = 'Test Account',
Phone = '03-1111-2222'
);
insert testAccount;
// 関連するContactを作成
List<Contact> contacts = new List<Contact>();
contacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact1',
AccountId = testAccount.Id,
Phone = '090-1111-1111'
));
contacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact2',
AccountId = testAccount.Id,
Phone = '090-2222-2222'
));
insert contacts;
// Accountの電話番号を更新
String newPhone = '03-9999-8888';
testAccount.Phone = newPhone;
Test.startTest();
update testAccount;
Test.stopTest();
// 関連するContactの電話番号が同期されていることを確認
List<Contact> updatedContacts = [
SELECT Id, Phone
FROM Contact
WHERE AccountId = :testAccount.Id
];
System.assertEquals(2, updatedContacts.size(), '2件のContactが存在するべき');
for (Contact c : updatedContacts) {
System.assertEquals(newPhone, c.Phone,
'ContactのPhoneがAccountのPhoneに同期されるべき');
}
}
/**
* Accountに関連するContactがない場合でもエラーが発生しないことを確認
*/
@isTest
static void testAccountPhoneUpdate_NoContacts_NoError() {
// ContactなしのAccountを作成
Account testAccount = new Account(
Name = 'Test Account No Contacts',
Phone = '03-1111-2222'
);
insert testAccount;
// Accountの電話番号を更新
testAccount.Phone = '03-9999-8888';
Test.startTest();
// エラーが発生しないことを確認
try {
update testAccount;
System.assert(true, 'Contactがなくてもエラーは発生しないべき');
} catch (Exception e) {
System.assert(false, '予期しないエラーが発生: ' + e.getMessage());
}
Test.stopTest();
}
/**
* 複数のAccountを一括更新した場合も正しく同期されることを確認
*/
@isTest
static void testBulkAccountPhoneUpdate_SyncsToContacts() {
// 複数のAccountを作成
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 5; i++) {
accounts.add(new Account(
Name = 'Test Account ' + i,
Phone = '03-0000-000' + i
));
}
insert accounts;
// 各Accountに関連するContactを作成
List<Contact> contacts = new List<Contact>();
for (Account acc : accounts) {
contacts.add(new Contact(
FirstName = 'Test',
LastName = 'Contact for ' + acc.Name,
AccountId = acc.Id,
Phone = '090-0000-0000'
));
}
insert contacts;
// 全Accountの電話番号を一括更新
for (Integer i = 0; i < accounts.size(); i++) {
accounts[i].Phone = '03-9999-999' + i;
}
Test.startTest();
update accounts;
Test.stopTest();
// 各ContactがそれぞれのAccountのPhoneに同期されていることを確認
Map<Id, Account> accountMap = new Map<Id, Account>(accounts);
List<Contact> updatedContacts = [
SELECT Id, Phone, AccountId
FROM Contact
WHERE AccountId IN :accountMap.keySet()
];
System.assertEquals(5, updatedContacts.size(), '5件のContactが存在するべき');
for (Contact c : updatedContacts) {
Account relatedAccount = accountMap.get(c.AccountId);
System.assertEquals(relatedAccount.Phone, c.Phone,
'ContactのPhoneが関連AccountのPhoneに同期されるべき');
}
}
}
このテストコードをデプロイしてテスト実行すると結果が得られる。(SalesforceのCLIでコントロールできる。)
結果の一部↓
=== Test Summary
NAME VALUE
─────────────────── ─────────────────────────────────
Outcome Passed
Tests Ran 11
Pass Rate 100%
Fail Rate 0%
Skip Rate 0%
Test Run Id 707gL00000UsMMd
Test Setup Time 0 ms
Test Execution Time 2528 ms
Test Total Time 2528 ms
Org Id 00DgL00000G7N0SUAV
Username [org]
ここで操作したレコードはロールバックされる模様。
所管
- こういう機能でテストできない領域もありそう
- このケースだとデータ操作を伴うテストを行っているが、処理が重そう&どうやら同時実行の制限がありそう?テストケースを増やすと破綻する?わからぬ
lwcのテスト
画面コンソールからではなくコードから画面のコンポーネントを作成できる。
実装の詳細は省くが、
こういうテンプレートに
<template>
<lightning-card title="取引先入力フォーム" icon-name="standard:account">
<!-- 入力フォーム -->
<div class="slds-p-around_medium">
<lightning-input
label="取引先名"
value={accountName}
onchange={handleNameChange}
placeholder="取引先名を入力してください"
required
></lightning-input>
<lightning-input
label="電話番号"
type="tel"
value={phone}
onchange={handlePhoneChange}
placeholder="03-1234-5678"
class="slds-m-top_small"
></lightning-input>
<lightning-input
label="業種"
value={industry}
onchange={handleIndustryChange}
placeholder="例: IT、製造業、小売業"
class="slds-m-top_small"
></lightning-input>
<div class="slds-m-top_medium">
<lightning-button
variant="brand"
label="追加"
onclick={handleAddAccount}
disabled={isAddDisabled}
></lightning-button>
<lightning-button
variant="neutral"
label="クリア"
onclick={handleClear}
class="slds-m-left_x-small"
></lightning-button>
</div>
</div>
<!-- 登録済み取引先リスト -->
<template lwc:if={hasAccounts}>
<div class="slds-p-around_medium slds-border_top">
<h3 class="slds-text-heading_small slds-m-bottom_small">
登録済み取引先 ({accountCount}件)
</h3>
<lightning-datatable
key-field="id"
data={accounts}
columns={columns}
hide-checkbox-column
onrowaction={handleRowAction}
></lightning-datatable>
</div>
</template>
<!-- 取引先がない場合 -->
<template lwc:else>
<div class="slds-p-around_medium slds-border_top">
<p class="slds-text-color_weak">まだ取引先が登録されていません。</p>
</div>
</template>
</lightning-card>
</template>
JavaScriptのふるまいを実装することで
import { LightningElement, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
export default class InputAccounts extends LightningElement {
// 入力フィールドの値
accountName = '';
phone = '';
industry = '';
// 取引先リスト
@track accounts = [];
// データテーブルの列定義
columns = [
{ label: '取引先名', fieldName: 'name', type: 'text' },
{ label: '電話番号', fieldName: 'phone', type: 'phone' },
{ label: '業種', fieldName: 'industry', type: 'text' },
{
type: 'action',
typeAttributes: {
rowActions: [
{ label: '削除', name: 'delete' }
]
}
}
];
// ゲッター: 追加ボタンの無効化判定
get isAddDisabled() {
return !this.accountName || this.accountName.trim() === '';
}
// ゲッター: 取引先があるかどうか
get hasAccounts() {
return this.accounts.length > 0;
}
// ゲッター: 取引先の件数
get accountCount() {
return this.accounts.length;
}
// イベントハンドラー: 取引先名の変更
handleNameChange(event) {
this.accountName = event.target.value;
}
// イベントハンドラー: 電話番号の変更
handlePhoneChange(event) {
this.phone = event.target.value;
}
// イベントハンドラー: 業種の変更
handleIndustryChange(event) {
this.industry = event.target.value;
}
// イベントハンドラー: 取引先の追加
handleAddAccount() {
if (this.isAddDisabled) {
return;
}
const newAccount = {
id: Date.now().toString(), // 一意のID
name: this.accountName.trim(),
phone: this.phone.trim(),
industry: this.industry.trim()
};
this.accounts = [...this.accounts, newAccount];
this.handleClear();
this.showToast('成功', '取引先を追加しました', 'success');
}
// イベントハンドラー: フォームのクリア
handleClear() {
this.accountName = '';
this.phone = '';
this.industry = '';
}
// イベントハンドラー: 行アクション(削除)
handleRowAction(event) {
const actionName = event.detail.action.name;
const row = event.detail.row;
if (actionName === 'delete') {
this.accounts = this.accounts.filter(account => account.id !== row.id);
this.showToast('成功', '取引先を削除しました', 'success');
}
}
// トースト通知を表示
showToast(title, message, variant) {
const event = new ShowToastEvent({
title: title,
message: message,
variant: variant
});
this.dispatchEvent(event);
}
}
こういうフォームをデプロイして利用できる。
これのテストをしたい場合は普通にJestのテストフレームワークを使ってテストが書ける・実行できる。
import { createElement } from '@lwc/engine-dom';
import InputAccounts from 'c/inputAccounts';
describe('c-input-accounts', () => {
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('コンポーネントが正しくレンダリングされる', () => {
const element = createElement('c-input-accounts', {
is: InputAccounts
});
document.body.appendChild(element);
const card = element.shadowRoot.querySelector('lightning-card');
expect(card).not.toBeNull();
expect(card.title).toBe('取引先入力フォーム');
});
it('取引先名が空の場合、追加ボタンが無効になる', () => {
const element = createElement('c-input-accounts', {
is: InputAccounts
});
document.body.appendChild(element);
const buttons = element.shadowRoot.querySelectorAll('lightning-button');
const addButton = Array.from(buttons).find(btn => btn.label === '追加');
expect(addButton).not.toBeNull();
expect(addButton.disabled).toBe(true);
});
it('初期状態では取引先リストが空であるメッセージが表示される', () => {
const element = createElement('c-input-accounts', {
is: InputAccounts
});
document.body.appendChild(element);
const emptyMessage = element.shadowRoot.querySelector('.slds-text-color_weak');
expect(emptyMessage).not.toBeNull();
expect(emptyMessage.textContent).toBe('まだ取引先が登録されていません。');
});
it('クリアボタンをクリックすると入力フィールドがリセットされる', async () => {
const element = createElement('c-input-accounts', {
is: InputAccounts
});
document.body.appendChild(element);
// 入力フィールドに値を設定
const inputs = element.shadowRoot.querySelectorAll('lightning-input');
const nameInput = Array.from(inputs).find(input => input.label === '取引先名');
// changeイベントを発火して値を設定
nameInput.dispatchEvent(new CustomEvent('change', {
target: { value: 'テスト株式会社' }
}));
await Promise.resolve();
// クリアボタンをクリック
const buttons = element.shadowRoot.querySelectorAll('lightning-button');
const clearButton = Array.from(buttons).find(btn => btn.label === 'クリア');
clearButton.click();
await Promise.resolve();
// 追加ボタンが無効になっていることでリセットを確認
const addButton = Array.from(buttons).find(btn => btn.label === '追加');
expect(addButton.disabled).toBe(true);
});
it('3つの入力フィールドが存在する', () => {
const element = createElement('c-input-accounts', {
is: InputAccounts
});
document.body.appendChild(element);
const inputs = element.shadowRoot.querySelectorAll('lightning-input');
expect(inputs.length).toBe(3);
});
});
ただこれはそもそもlwcを使って画面コンポーネントを実装しているケースはテストできるが、
画面コンソールからノーコードで画面制作しているケースは使えない模様。
実態運用的には用途が限られそう。
UTAM(Salesforce提供のE2Eテストフレームワーク)
WebDriverをベースにSalesforceに一部適応させたもののよう。
イマイチ実行環境を構築するのが面倒。
あんまり利用されている様子がない。
普通のE2Eテストフレームワークとの違いが
- メンテナンス:Salesforce が年3回のリリースで DOM を更新した場合、関連する JSON ファイルを更新するだけで済みます。数十、数百ものテストスクリプトを検索する必要がなくなり、テストのメンテナンスがより迅速かつ効率的になります。
- Shadow DOMへのアクセス:Lightning Web Components(LWC)はShadow DOMを使用しており、これにより内部構造が隠蔽されます。標準ツールではUIのこの部分にアクセスできません。UTAMにはShadow DOMをナビゲートするためのサポートが組み込まれているため、カスタムソリューションは不要です。
(引用: https://gearset.com/blog/salesforce-ui-testing-tools/)
とのことだが。
前者についてはSalesforceのアップデートに併せて変わるコンポーネント部品へのロケーター情報を併せて更新してくれるので、そのzipを更新するだけで対応できる、という謳い文句だが、独自に作った画面では追従されないようなので、うーんという感じ。(https://utam.dev/tools/browser-extension)
ShadowDOMとかもPlaywirghtなどのテストフレームワークのロケーターをうまく使うことができれば別に解決されていそうな気がするので、わざわざこれを運用する強力なメリットはわかっていない。
OSS的にもあまり人気がない。
https://github.com/salesforce/utam-java
E2Eの自動テストサービス
広く利用されているE2Eの自動テストサービスでもいいし、
Salesforceに特化したE2Eの自動テストサービスもたくさんある。
そのあたりのベンダーと契約して運用するのがよくやり方として取られていそう。
お金で解決できる。(相応に高いコストになりそう。)
E2Eの自動テストサービスベンダーもそのあたりの運用知見もあるので、コンサルティングも含め受けて運用されているところはやっているんだろう。
E2Eだけですべてのテストケースを運用することはテストピラミッドなどの考え方から望ましくなさそうに感じるが、そのあたりもなんとかする知見をお持ちなんだろう。
実際の環境に対して操作しながらテストする場合、データがどんどん溜まっていってテストが安定しなさそうなので、そのあたりはsandboxを簡単に作成できるSalesforceの特徴を使って、E2E実行毎に環境を作成して前処理とする、みたいなことをやられている。さすがクラメソさん。(この運用もかなり難しそう)
Playwrightを利用したテスト
一応通常のE2Eテストフレームワークを利用したテストも実装できる。
作業途中。
ちょっとロケーターとか破たん感見え見えなのでどうしたもんか。
サンプルでコードをおいてみた。
https://github.com/theMistletoe/Salesforce_sample/tree/main/playwright



