この文章は Salesforce 機械翻訳システムを使用して翻訳されました。詳細はこちらをご参照ください。
英語に切り替える

拡張トランザクションセキュリティの Apex テスト

堅牢なテストを記述することは、コードが期待どおりに動作することを確認し、ユーザおよび顧客が実行する前にエラーを発見するためのエンジニアリング上のベストプラクティスです。トランザクションセキュリティポリシーの Apex コードは Salesforce 組織で重要なユーザアクションを実行するため、そのテストを記述することはさらに重要です。たとえば、テスト中 LoginEvent ポリシーのバグがキャッチされなかった場合、ユーザが組織から締め出される可能性もありますが、これは避けるべき状況です。
使用可能なインターフェース: Salesforce Classic および Lightning Experience
使用可能なエディション: Enterprise Edition、Unlimited Edition、および Developer Edition

Salesforce Shield または Salesforce Event Monitoring アドオンサブスクリプションが必要です。


拡張トランザクションセキュリティポリシーの Apex テストを記述するときは、API バージョン 47.0 以降を使用します。

警告

一連の条件をシミュレートして Apex コードをテストする場合は、当然、単体テストを記述します。ただし、単体テストを記述するだけでは不十分です。ビジネスチームおよびセキュリティチームと協力して、あらゆる使用事例を理解してください。その後、Sandbox 環境のテストデータを使用して実際のユーザの体験を模した包括的なテスト計画を作成します。テスト計画には通常、手動テストと、Selenium などの外部ツールを使用する自動テストの両方が含まれます。

開始するために単体テストの例を見てみましょう。次の Apex ポリシーをテストするとします。

1global class LeadExportEventCondition implements TxnSecurity.EventCondition {
2    public boolean evaluate(SObject event) {
3        switch on event{
4            when ApiEvent apiEvent {
5                return evaluate(apiEvent.QueriedEntities, apiEvent.RowsProcessed);
6            }
7            when ReportEvent reportEvent {
8                return evaluate(reportEvent.QueriedEntities, reportEvent.RowsProcessed);
9            }
10            when ListViewEvent listViewEvent {
11                return evaluate(listViewEvent.QueriedEntities, listViewEvent.RowsProcessed);
12            }
13            when null {
14                 return false;   
15            }
16            when else {
17                return false;
18            }
19        }
20    }
21
22    private boolean evaluate(String queriedEntities, Decimal rowsProcessed){
23        if (queriedEntities.contains('Lead') && rowsProcessed > 2000){
24            return true;
25        }
26        return false;
27    }
28}

テストの計画および記述

テストの記述を開始する前に、テスト計画で対象とするプラスとマイナスの使用事例の概要を確認しましょう。

表 1. ポジティブテストケース
evaluate メソッドが受信した場合... かつ... evaluate メソッドが返す...
ApiEvent オブジェクト ApiEvent で Lead がその QueriedEntities 項目にあり、2000 より大きい数値が RowsProcessed 項目にある true
ReportEvent オブジェクト ReportEvent で Lead がその QueriedEntities 項目にあり、2000 より大きい数値が RowsProcessed 項目にある true
ListViewEvent オブジェクト ListViewEvent で Lead がその QueriedEntities 項目にあり、2000 より大きい数値が RowsProcessed 項目にある true
任意のイベントオブジェクト イベントで Lead がその QueriedEntities 項目になく、2000 より大きい数値が RowsProcessed 項目にある false
任意のイベントオブジェクト イベントで Lead がその QueriedEntities 項目にあり、2000 以下の数値が RowsProcessed 項目にある false
任意のイベントオブジェクト イベントで Lead がその QueriedEntities 項目になく、2000 以下の数値が RowsProcessed 項目にある false
表 2. ネガティブテストケース
evaluate メソッドが受信した場合... かつ... evaluate メソッドが返す...
LoginEvent オブジェクト (条件なし) false
null 値 (条件なし) false
ApiEvent オブジェクト QueriedEntities 項目が null である false
ReportEvent オブジェクト RowsProcessed 項目が null である false

次に、こうしたすべての使用事例を実装する Apex テストコードを示します。

1/**
2 * Tests for the LeadExportEventCondition class, to make sure that our Transaction Security Apex 
3 * logic handles events and event field values as expected.
4 **/
5 @isTest
6 public class LeadExportEventConditionTest {
7 
8    /**
9     * ------------ POSITIVE TEST CASES ------------
10     ** /
11 
12     /**
13      * Positive test case 1: If an ApiEvent has Lead as a queried entity and more than 2000 rows 
14      * processed, then the evaluate method of our policy's Apex should return true.
15      **/ 
16      static testMethod void testApiEventPositiveTestCase() {
17          // set up our event and its field values
18          ApiEvent testEvent = new ApiEvent();
19          testEvent.QueriedEntities = 'Account, Lead';
20          testEvent.RowsProcessed = 2001;
21          
22          // test that the Apex returns true for this event
23          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
24          System.assert(eventCondition.evaluate(testEvent));   
25      }
26     
27     /**
28      * Positive test case 2: If a ReportEvent has Lead as a queried entity and more than 2000 rows 
29      * processed, then the evaluate method of our policy's Apex should return true.
30      **/ 
31      static testMethod void testReportEventPositiveTestCase() {
32          // set up our event and its field values
33          ReportEvent testEvent = new ReportEvent();
34          testEvent.QueriedEntities = 'Account, Lead';
35          testEvent.RowsProcessed = 2001;
36          
37          // test that the Apex returns true for this event
38          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
39          System.assert(eventCondition.evaluate(testEvent));   
40      }
41     
42     /**
43      * Positive test case 3: If a ListViewEvent has Lead as a queried entity and more than 2000 rows 
44      * processed, then the evaluate method of our policy's Apex should return true.
45      **/ 
46      static testMethod void testListViewEventPositiveTestCase() {
47          // set up our event and its field values
48          ListViewEvent testEvent = new ListViewEvent();
49          testEvent.QueriedEntities = 'Account, Lead';
50          testEvent.RowsProcessed = 2001;
51          
52          // test that the Apex returns true for this event
53          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
54          System.assert(eventCondition.evaluate(testEvent));   
55      }
56     
57     /**
58      * Positive test case 4: If an event does not have Lead as a queried entity and has more 
59      * than 2000 rows processed, then the evaluate method of our policy's Apex 
60      * should return false.
61      **/ 
62      static testMethod void testOtherQueriedEntityPositiveTestCase() {
63          // set up our event and its field values
64          ApiEvent testEvent = new ApiEvent();
65          testEvent.QueriedEntities = 'Account';
66          testEvent.RowsProcessed = 2001;
67          
68          // test that the Apex returns false for this event
69          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
70          System.assertEquals(false, eventCondition.evaluate(testEvent));   
71      }
72      
73    /**
74      * Positive test case 5: If an event has Lead as a queried entity and does not have 
75      * more than 2000 rows processed, then the evaluate method of our policy's Apex 
76      * should return false.
77      **/ 
78      static testMethod void testFewerRowsProcessedPositiveTestCase() {
79          // set up our event and its field values
80          ReportEvent testEvent = new ReportEvent();
81          testEvent.QueriedEntities = 'Account, Lead';
82          testEvent.RowsProcessed = 2000;
83          
84          // test that the Apex returns false for this event
85          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
86          System.assertEquals(false, eventCondition.evaluate(testEvent));   
87      }
88      
89    /**
90      * Positive test case 6: If an event does not have Lead as a queried entity and does not have 
91      * more than 2000 rows processed, then the evaluate method of our policy's Apex 
92      * should return false.
93      **/ 
94      static testMethod void testNoConditionsMetPositiveTestCase() {
95          // set up our event and its field values
96          ListViewEvent testEvent = new ListViewEvent();
97          testEvent.QueriedEntities = 'Account';
98          testEvent.RowsProcessed = 2000;
99          
100          // test that the Apex returns false for this event
101          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
102          System.assertEquals(false, eventCondition.evaluate(testEvent));   
103      }
104      
105      /**
106       * ------------ NEGATIVE TEST CASES ------------
107       **/
108     
109     /**
110      * Negative test case 1: If an event is a type other than ApiEvent, ReportEvent, or ListViewEvent,
111      * then the evaluate method of our policy's Apex should return false.
112      **/
113      static testMethod void testOtherEventObject() {
114          LoginEvent loginEvent = new LoginEvent();
115          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
116          System.assertEquals(false, eventCondition.evaluate(loginEvent));   
117      } 
118 
119     /**
120      * Negative test case 2: If an event is null, then the evaluate method of our policy's
121      * Apex should return false.
122      **/
123      static testMethod void testNullEventObject() {
124          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
125          System.assertEquals(false, eventCondition.evaluate(null));   
126      } 
127     
128     /**
129      * Negative test case 3: If an event has a null QueriedEntities value, then the evaluate method 
130      * of our policy's Apex should return false.
131      **/
132      static testMethod void testNullQueriedEntities() {
133          ApiEvent testEvent = new ApiEvent(); 
134          testEvent.QueriedEntities = null;
135          testEvent.RowsProcessed = 2001;
136          
137          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
138          System.assertEquals(false, eventCondition.evaluate(testEvent));   
139      }
140     
141     /**
142      * Negative test case 4: If an event has a null RowsProcessed value, then the evaluate method 
143      * of our policy's Apex should return false.
144      **/
145      static testMethod void testNullRowsProcessed() {
146          ReportEvent testEvent = new ReportEvent(); 
147          testEvent.QueriedEntities = 'Account, Lead';
148          testEvent.RowsProcessed = null;
149          
150          LeadExportEventCondition  eventCondition = new LeadExportEventCondition();
151          System.assertEquals(false, eventCondition.evaluate(testEvent));   
152      } 
153 }

テスト実行後のポリシーコードの調整

テストを実行し、testNullQueriedEntities テストケースが失敗してエラー System.NullPointerException: Attempt to de-reference a null object が起こったとします。幸い、テストで予期しない値または null 値をチェックしないトランザクションセキュリティポリシーの領域が明らかになりました。ポリシーは重要な組織の操作中に実行されるため、重要な機能がブロックされないようにエラーがある場合はポリシーが適切に失敗することを確認します。

こうした null 値を適切に処理する Apex クラスの evaluate メソッドを更新する方法を次に示します。

1private boolean evaluate(String queriedEntities, Decimal rowsProcessed) {
2    boolean containsLead = queriedEntities != null ? queriedEntities.contains('Lead')
3    if (containsLead && rowsProcessed > 2000){
4        return true;
5    }
6    return false;
7}

queriedEntities 変数で .contains 操作を実行する前に値が null かどうかを最初にチェックするように、コードを変更しました。この変更により、コードで null オブジェクトが参照解決���れなくなります。

通常、Apex コードで予期しない値または状況に遭遇した場合、2 つのオプションがあります。
  • 値または状況を無視して、ポリシーがトリガしないように false を返す。
  • true を返して操作をフェイルクローズする。
どちらのオプションを選択するか決定するときは、ユーザに最適な方法を判断します。

高度な例

ログインしようとしているユーザのプロファイルを取得する SOQL クエリを使用するより複雑な Apex ポリシーを次に示します。

1global class ProfileIdentityEventCondition implements TxnSecurity.EventCondition {
2
3    // For these powerful profiles, let's prompt users to complete 2FA
4    private Set<String> PROFILES_TO_MONITOR = new Set<String> { 
5        'System Administrator', 
6        'Custom Admin Profile'
7    };
8    
9    public boolean evaluate(SObject event) {
10        LoginEvent loginEvent = (LoginEvent) event;
11        String userId = loginEvent.UserId;
12        
13        // get the Profile name from the current users profileId
14        Profile profile = [SELECT Name FROM Profile WHERE Id IN 
15                    (SELECT profileId FROM User WHERE Id = :userId)];
16        
17        // check if the name of the Profile is one of the ones we want to monitor
18        if (PROFILES_TO_MONITOR.contains(profile.Name)) {
19            return true;
20        }
21        
22        return false;
23    }   
24 }

テスト計画は次のようになります。

  • ポジティブテストケース
    • ログインしようとしているユーザのプロファイルを監視したい場合は、evaluate メソッドが true を返す。
    • ログインしようとしているユーザのプロファイルを監視したくない場合は、evaluate メソッドが false を返す。
  • ネガティブテストケース
    • 例外を発生させるプロファイルオブジェクトのクエリを行う場合は、evaluate メソッドが false を返す。
    • null を返すプロファイルオブジェクトのクエリを行う場合は、evaluate メソッドが false を返す。

すべての Salesforce ユーザに必ずプロファイルが割り当てられるため、そのネガティブテストを作成する必要はありません。2 つのネガティブテストケースに実際のテストを作成することもできません。これについては、ポリシー自体を更新することで Salesforce が行います。ただし、計画で使用事例を明示歴にリストし、さまざまな状況に対応できるようにします。

ポジティブテストケースは、SQQL クエリの結果のみに依存します。これらのクエリが正しく実行されるようにするために、テストデータも作成します。テストコードを見てみましょう。

1/**
2 * Tests for the ProfileIdentityEventCondition class, to make sure that our 
3 * Transaction Security Apex logic handles events and event field values as expected.
4 **/
5 @isTest
6 public class ProfileIdentityEventConditionTest {
7 
8    /**
9     * ------------ POSITIVE TEST CASES ------------
10     ** /
11 
12     /**
13      * Positive test case 1: Evaluate will return true when user has the "System 
14      * Administrator" profile.
15      **/ 
16      static testMethod void testUserWithSysAdminProfile() {
17          // insert a User for our test which has the System Admin profile
18          Profile profile = [SELECT Id FROM Profile WHERE Name='System Administrator'];
19          assertOnProfile(profile.id, true); 
20      }
21
22     /**
23      * Positive test case 2: Evaluate will return true when the user has the "Custom
24      * Admin Profile"
25      **/ 
26      static testMethod void testUserWithCustomProfile() {
27          // insert a User for our test which has the System Admin profile
28          Profile profile = [SELECT Id FROM Profile WHERE Name='Custom Admin Profile'];
29          assertOnProfile(profile.id, true);
30      }
31      
32     /**
33      * Positive test case 3: Evalueate will return false when user doesn't have
34      * a profile we're interested in. In this case we'll be using a profile called
35      * 'Standard User'.
36      **/ 
37      static testMethod void testUserWithSomeProfile() {
38          // insert a User for our test which has the System Admin profile
39          Profile profile = [SELECT Id FROM Profile WHERE Name='Standard User'];
40          assertOnProfile(profile.id, false);
41      }
42      
43      /**
44       * Helper to assert on different profiles.
45       **/
46      static void assertOnProfile(String profileId, boolean expected){
47          User user = createUserWithProfile(profileId);
48          insert user;
49      
50          // set up our event and its field values
51          LoginEvent testEvent = new LoginEvent();
52          testEvent.UserId = user.Id;
53          
54          // test that the Apex returns true for this event
55          ProfileIdentityEventCondition  eventCondition = new ProfileIdentityEventCondition();
56          System.assertEquals(expected, eventCondition.evaluate(testEvent));  
57      }
58      
59      /**
60       * Helper to create a user with the given profileId.
61       **/
62      static User createUserWithProfile(String profileId){
63          // Usernames have to be unique.
64          String username = 'ProfileIdentityEventCondition@Test.com';
65          
66          User user = new User(Alias = 'standt', Email='standarduser@testorg.com', 
67          EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', 
68          LocaleSidKey='en_US', ProfileId = profileId, 
69          TimeZoneSidKey='America/Los_Angeles', UserName=username);
70          return user;
71      }
72 }

プロファイルオブジェクトのクエリを行うときに例外または null の結果をチェックするようにトランザクションセキュリティポリシーコードを更新して、2 つのネガティブテストケースを処理しましょう。

1global class ProfileIdentityEventCondition implements TxnSecurity.EventCondition {
2
3    // For these powerful profiles, let's prompt users to complete 2FA
4    private Set<String> PROFILES_TO_MONITOR = new Set<String> { 
5        'System Administrator', 
6        'Custom Admin Profile'
7    };
8    
9    public boolean evaluate(SObject event) {
10        try{
11            LoginEvent loginEvent = (LoginEvent) event;
12            String userId = loginEvent.UserId;
13            
14            // get the Profile name from the current users profileId
15            Profile profile = [SELECT Name FROM Profile WHERE Id IN 
16                        (SELECT profileId FROM User WHERE Id = :userId)];
17            
18            if (profile == null){
19                return false;
20            }
21            
22            // check if the name of the Profile is one of the ones we want to monitor
23            if (PROFILES_TO_MONITOR.contains(profile.Name)) {
24                return true;
25            }
26            return false;
27        } catch(Exception ex){
28            System.debug('Exception: ' + ex);
29            return false;   
30        }
31    }   
32 }