0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Salesforce勉強メモ

Last updated at Posted at 2026-01-10

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%

踏まえると(実態運用を踏まえられていないので欠陥はありそうだが)以下の考え方の方が良さそう(という仮説)。

  1. サーバーサイドのロジックやフローのふるまいに対する検証でApexによるテストで対応できるケースは可能な限りApexによるテストコードによるテストを行う。
  2. 画面フローなどフロントエンドに関連する部分のテストはLWCを利用していない場合独立したテストを実行できないため、統合テストでコンポーネント間の連携と併せて、E2Eテスト(PlaywrightやProvar)で重要なユーザーフローに限定したテストを行う。

各Salseforceのテスト実装・機能について

以降、Salesforceで利用できそうなテスト実装・機能についてまとめる。

フローのテスト機能

フロー作成時の画面でテスト機能がある。

取引先責任者更新_-_V6.png

複数のテストケースが作成できる。

取引先責任者更新_-_V6.png

各テストケースにアサーションを設定して保存できるようだが、アサーションの種類が乏しく、ビジネス要件をテストしたいケースをおそらくほとんど満たせない気がするので、あまり出番は無さそうな印象。

取引先責任者更新_-_V6.png

あと、画面フローではこの機能を使えない。

参考:
https://lanefour.com/salesforce-admin/salesforce-flow-tests-5-scenarios-where-apex-unit-tests-are-the-better-choice/

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

こういうフォームをデプロイして利用できる。

test___取引先___Salesforce.png

これのテストをしたい場合は普通に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

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?