Sie befinden sich hier:
Verbesserte Apex-Transaktionssicherheitstests
Das Schreiben zuverlässiger Tests ist eine bewährte Vorgehensweise, um sicherzustellen, dass sich Ihr Code erwartungsgemäß verhält, und um Fehler zu finden, bevor Ihre Benutzer und Kunden es tun. Es ist sogar noch wichtiger, Tests für den Apex-Code Ihrer Transaktionssicherheitsrichtlinie zu schreiben, da er bei wichtigen Benutzeraktionen in Ihrer Salesforce-Organisation ausgeführt wird. Beispielsweise kann ein Fehler in Ihrer LoginEvent-Richtlinie, der während des Tests nicht erkannt wird, dazu führen, dass Ihre Benutzer aus Ihrer Organisation ausgesperrt werden, was natürlich vermieden werden sollte.
Erforderliche Editionen
| Verfügbar in Salesforce Classic (nicht in allen Organisationen verfügbar) und Lightning Experience. |
Verfügbarkeit: Enterprise, Unlimited und Developer Edition Hierfür sind Add-On-Abonnements für Salesforce Shield oder die Salesforce-Ereignisüberwachung erforderlich. |
Wenn Sie Ihren Apex-Code testen, indem Sie eine Reihe von Bedingungen simulieren, schreiben Sie definitionsgemäß Einheitentests. Das Schreiben von Einheitentests ist jedoch nicht genug. Arbeiten Sie mit Ihren Geschäfts- und Sicherheitsteams zusammen, um alle Ihre Anwendungsfälle zu verstehen. Erstellen Sie anschließend mithilfe von Testdaten in einer Sandbox-Umgebung einen umfassenden Testplan, der die Erfahrung Ihrer tatsächlichen Benutzer nachahmt. Der Testplan beinhaltet in der Regel sowohl manuelle Tests als auch automatisierte Tests mithilfe externer Tools wie Selenium.
Im Folgenden finden Sie für den Einstieg einige Beispiele für Einheitentests. Hier finden Sie die Apex-Richtlinie, die getestet werden soll.
global class LeadExportEventCondition implements TxnSecurity.EventCondition {
public boolean evaluate(SObject event) {
switch on event{
when ApiEvent apiEvent {
return evaluate(apiEvent.QueriedEntities, apiEvent.RowsProcessed);
}
when ReportEvent reportEvent {
return evaluate(reportEvent.QueriedEntities, reportEvent.RowsProcessed);
}
when ListViewEvent listViewEvent {
return evaluate(listViewEvent.QueriedEntities, listViewEvent.RowsProcessed);
}
when null {
return false;
}
when else {
return false;
}
}
}
private boolean evaluate(String queriedEntities, Decimal rowsProcessed){
if (queriedEntities.contains('Lead') && rowsProcessed > 2000){
return true;
}
return false;
}
}
Planen und Schreiben von Tests
Bevor mit dem Schreiben der Tests begonnen wird, sollten die positiven und negativen Anwendungsfälle hervorgehoben werden, die von unserem Testplan abgedeckt werden.
Empfängt die Methode evaluate Folgendes … |
Und … | Dann gibt die Methode evaluate Folgendes zurück … |
|---|---|---|
| Ein "ApiEvent"-Objekt | Das Objekt "ApiEvent" verfügt über "Lead" im zugehörigen Feld "QueriedEntities" und im Feld "RowsProcessed" über eine Zahl, die größer als 2000 ist. |
true |
| Ein "ReportEvent"-Objekt | Das Objekt "ReportEvent" verfügt über "Lead" im zugehörigen Feld "QueriedEntities" und im Feld "RowsProcessed" über eine Zahl, die größer als 2000 ist. |
true
|
| Ein ListViewEvent-Objekt | Das Objekt "ListViewEvent" verfügt über "Lead" im zugehörigen Feld "QueriedEntities" und im Feld "RowsProcessed" über eine Zahl, die größer als 2000 ist. |
true
|
| Ein beliebiges Ereignisobjekt | Das Ereignis verfügt nicht über "Lead" im zugehörigen Feld "QueriedEntities" und verfügt im Feld "RowsProcessed" über eine Zahl, die größer als 2000 ist. |
false
|
| Ein beliebiges Ereignisobjekt | Das Ereignis verfügt über "Lead" im zugehörigen Feld "QueriedEntities" und im Feld "RowsProcessed" über eine Zahl, die kleiner oder gleich 2000 ist. |
false
|
| Ein beliebiges Ereignisobjekt | Das Ereignis verfügt nicht über "Lead" im zugehörigen Feld "QueriedEntities" und verfügt im Feld "RowsProcessed" über eine Zahl, die kleiner oder gleich 2000 ist. |
false
|
Empfängt die Methode evaluate Folgendes … |
Und … | Dann gibt die Methode evaluate Folgendes zurück … |
|---|---|---|
| Ein LoginEvent-Objekt | (ohne Bedingung) |
false
|
| Ein Nullwert | (ohne Bedingung) |
false
|
| Ein "ApiEvent"-Objekt | Das Feld QueriedEntities entspricht "null" |
false
|
| Ein "ReportEvent"-Objekt | Das Feld RowsProcessed entspricht "null" |
false
|
Im Folgenden finden Sie den Apex-Testcode, der alle diese Anwendungsfälle implementiert.
/**
* Tests for the LeadExportEventCondition class, to make sure that our Transaction Security Apex
* logic handles events and event field values as expected.
**/
@isTest
public class LeadExportEventConditionTest {
/**
* ------------ POSITIVE TEST CASES ------------
** /
/**
* Positive test case 1: If an ApiEvent has Lead as a queried entity and more than 2000 rows
* processed, then the evaluate method of our policy's Apex should return true.
**/
static testMethod void testApiEventPositiveTestCase() {
// set up our event and its field values
ApiEvent testEvent = new ApiEvent();
testEvent.QueriedEntities = 'Account, Lead';
testEvent.RowsProcessed = 2001;
// test that the Apex returns true for this event
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assert(eventCondition.evaluate(testEvent));
}
/**
* Positive test case 2: If a ReportEvent has Lead as a queried entity and more than 2000 rows
* processed, then the evaluate method of our policy's Apex should return true.
**/
static testMethod void testReportEventPositiveTestCase() {
// set up our event and its field values
ReportEvent testEvent = new ReportEvent();
testEvent.QueriedEntities = 'Account, Lead';
testEvent.RowsProcessed = 2001;
// test that the Apex returns true for this event
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assert(eventCondition.evaluate(testEvent));
}
/**
* Positive test case 3: If a ListViewEvent has Lead as a queried entity and more than 2000 rows
* processed, then the evaluate method of our policy's Apex should return true.
**/
static testMethod void testListViewEventPositiveTestCase() {
// set up our event and its field values
ListViewEvent testEvent = new ListViewEvent();
testEvent.QueriedEntities = 'Account, Lead';
testEvent.RowsProcessed = 2001;
// test that the Apex returns true for this event
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assert(eventCondition.evaluate(testEvent));
}
/**
* Positive test case 4: If an event does not have Lead as a queried entity and has more
* than 2000 rows processed, then the evaluate method of our policy's Apex
* should return false.
**/
static testMethod void testOtherQueriedEntityPositiveTestCase() {
// set up our event and its field values
ApiEvent testEvent = new ApiEvent();
testEvent.QueriedEntities = 'Account';
testEvent.RowsProcessed = 2001;
// test that the Apex returns false for this event
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assertEquals(false, eventCondition.evaluate(testEvent));
}
/**
* Positive test case 5: If an event has Lead as a queried entity and does not have
* more than 2000 rows processed, then the evaluate method of our policy's Apex
* should return false.
**/
static testMethod void testFewerRowsProcessedPositiveTestCase() {
// set up our event and its field values
ReportEvent testEvent = new ReportEvent();
testEvent.QueriedEntities = 'Account, Lead';
testEvent.RowsProcessed = 2000;
// test that the Apex returns false for this event
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assertEquals(false, eventCondition.evaluate(testEvent));
}
/**
* Positive test case 6: If an event does not have Lead as a queried entity and does not have
* more than 2000 rows processed, then the evaluate method of our policy's Apex
* should return false.
**/
static testMethod void testNoConditionsMetPositiveTestCase() {
// set up our event and its field values
ListViewEvent testEvent = new ListViewEvent();
testEvent.QueriedEntities = 'Account';
testEvent.RowsProcessed = 2000;
// test that the Apex returns false for this event
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assertEquals(false, eventCondition.evaluate(testEvent));
}
/**
* ------------ NEGATIVE TEST CASES ------------
**/
/**
* Negative test case 1: If an event is a type other than ApiEvent, ReportEvent, or ListViewEvent,
* then the evaluate method of our policy's Apex should return false.
**/
static testMethod void testOtherEventObject() {
LoginEvent loginEvent = new LoginEvent();
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assertEquals(false, eventCondition.evaluate(loginEvent));
}
/**
* Negative test case 2: If an event is null, then the evaluate method of our policy's
* Apex should return false.
**/
static testMethod void testNullEventObject() {
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assertEquals(false, eventCondition.evaluate(null));
}
/**
* Negative test case 3: If an event has a null QueriedEntities value, then the evaluate method
* of our policy's Apex should return false.
**/
static testMethod void testNullQueriedEntities() {
ApiEvent testEvent = new ApiEvent();
testEvent.QueriedEntities = null;
testEvent.RowsProcessed = 2001;
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assertEquals(false, eventCondition.evaluate(testEvent));
}
/**
* Negative test case 4: If an event has a null RowsProcessed value, then the evaluate method
* of our policy's Apex should return false.
**/
static testMethod void testNullRowsProcessed() {
ReportEvent testEvent = new ReportEvent();
testEvent.QueriedEntities = 'Account, Lead';
testEvent.RowsProcessed = null;
LeadExportEventCondition eventCondition = new LeadExportEventCondition();
System.assertEquals(false, eventCondition.evaluate(testEvent));
}
}Eingrenzen des Richtliniencodes nach dem Ausführen der Tests
Angenommen, Sie führen die Tests aus und der Testkundenvorgang "testNullQueriedEntities" schlägt mit der Fehler "System.NullPointerException: Attempt to de-reference a null
object" fehl. Gute Neuigkeiten: Die Tests haben einen Bereich der Transaktionssicherheitsrichtlinie identifiziert, in dem keine Überprüfungen auf unerwartete oder Nullwerte vorgenommen werden. Da Richtlinien während wichtiger Organisationsvorgänge ausgeführt werden, sollten Sie sicherstellen, dass die Richtlinien kontrolliert fehlschlagen, falls ein Fehler vorliegt, damit sie keine wichtigen Funktionen blockieren.
Im Folgenden erfahren Sie, wie Sie die Methode "evaluate" in der Apex-Klasse aktualisieren, um diese Nullwerte kontrolliert zu verarbeiten.
private boolean evaluate(String queriedEntities, Decimal rowsProcessed) {
boolean containsLead = queriedEntities != null ? queriedEntities.contains('Lead') : false;
if (containsLead && rowsProcessed > 2000){
return true;
}
return false;
}Der Code wurde geändert, sodass zunächst überprüft wird, ob der Wert null ist, bevor der Vorgang ".contains" in der Variablen "queriedEntities" durchgeführt wird. Durch diese Änderung wird sichergestellt, dass der Code den Verweis auf ein Nullobjekt nicht aufhebt.
Wenn Sie im Allgemeinen unerwartete Werte oder Situationen in Ihrem Apex-Code ermitteln, haben Sie zwei Möglichkeiten. Ermitteln Sie, was für Ihre Benutzer am besten ist, wenn Sie festlegen, welche Option ausgewählt werden soll:
- Ignorieren Sie die Werte oder Situation und geben Sie "
false" zurück, damit die Richtlinie nicht ausgelöst wird. - Veranlassen Sie, dass der Vorgang fehlerhaft geschlossen wird, indem "
true" zurückgegeben wird.
Erweitertes Beispiel
Im Folgenden finden Sie eine komplexere Apex-Richtlinie, die SOQL-Abfragen verwendet, um das Profil des Benutzers abzurufen, der versucht, sich anzumelden.
global class ProfileIdentityEventCondition implements TxnSecurity.EventCondition {
// For these powerful profiles, let's prompt users to complete 2FA
private Set<String> PROFILES_TO_MONITOR = new Set<String> {
'System Administrator',
'Custom Admin Profile'
};
public boolean evaluate(SObject event) {
LoginEvent loginEvent = (LoginEvent) event;
String userId = loginEvent.UserId;
// get the Profile name from the current users profileId
Profile profile = [SELECT Name FROM Profile WHERE Id IN
(SELECT profileId FROM User WHERE Id = :userId)];
// check if the name of the Profile is one of the ones we want to monitor
if (PROFILES_TO_MONITOR.contains(profile.Name)) {
return true;
}
return false;
}
}Im Folgenden ist unser Testplan für positive Testfälle beschrieben:
-
- Wenn der Benutzer, der versucht, sich anzumelden, über das zu überwachende Profil verfügt, gibt die "
evaluate"-Methode "true" zurück. - Wenn der Benutzer, der versucht, sich anzumelden, nicht über das zu überwachende Profil verfügt, gibt die "
evaluate"-Methode "false" zurück.
- Wenn der Benutzer, der versucht, sich anzumelden, über das zu überwachende Profil verfügt, gibt die "
Und hier unser Plan für negative Testfälle:
-
- Wenn die Abfrage nach dem Profilobjekt eine Ausnahme auslöst, gibt die "
evaluate"-Methode "false" zurück. - Wenn die Abfrage nach dem Profilobjekt null zurückgibt, gibt die "
evaluate"-Methode "false" zurück.
- Wenn die Abfrage nach dem Profilobjekt eine Ausnahme auslöst, gibt die "
Da jedem Salesforce-Benutzer immer ein Profil zugewiesen ist, müssen Sie dafür keinen negativen Test erstellen. Es ist zudem nicht möglich, tatsächliche Tests für die beiden negativen Testfälle zu erstellen. Wir erledigen diese Maßnahmen, indem wir die Richtlinie selbst aktualisieren. Die Anwendungsfälle in unserem Plan werden jedoch explizit aufgelistet, um sicherzustellen, dass viele verschiedene Situationen abgedeckt sind.
Die positiven Testfälle basieren auf den Ergebnissen von SQQL-Abfragen. Um sicherzustellen, dass diese Abfragen richtig ausgeführt werden, müssen zudem einige Testdaten erstellt werden. Im Folgenden finden Sie Testcode.
/**
* Tests for the ProfileIdentityEventCondition class, to make sure that our
* Transaction Security Apex logic handles events and event field values as expected.
**/
@isTest
public class ProfileIdentityEventConditionTest {
/**
* ------------ POSITIVE TEST CASES ------------
** /
/**
* Positive test case 1: Evaluate will return true when user has the "System
* Administrator" profile.
**/
static testMethod void testUserWithSysAdminProfile() {
// insert a User for our test which has the System Admin profile
Profile profile = [SELECT Id FROM Profile WHERE Name='System Administrator'];
assertOnProfile(profile.id, true);
}
/**
* Positive test case 2: Evaluate will return true when the user has the "Custom
* Admin Profile"
**/
static testMethod void testUserWithCustomProfile() {
// insert a User for our test which has the System Admin profile
Profile profile = [SELECT Id FROM Profile WHERE Name='Custom Admin Profile'];
assertOnProfile(profile.id, true);
}
/**
* Positive test case 3: Evalueate will return false when user doesn't have
* a profile we're interested in. In this case we'll be using a profile called
* 'Standard User'.
**/
static testMethod void testUserWithSomeProfile() {
// insert a User for our test which has the System Admin profile
Profile profile = [SELECT Id FROM Profile WHERE Name='Standard User'];
assertOnProfile(profile.id, false);
}
/**
* Helper to assert on different profiles.
**/
static void assertOnProfile(String profileId, boolean expected){
User user = createUserWithProfile(profileId);
insert user;
// set up our event and its field values
LoginEvent testEvent = new LoginEvent();
testEvent.UserId = user.Id;
// test that the Apex returns true for this event
ProfileIdentityEventCondition eventCondition = new ProfileIdentityEventCondition();
System.assertEquals(expected, eventCondition.evaluate(testEvent));
}
/**
* Helper to create a user with the given profileId.
**/
static User createUserWithProfile(String profileId){
// Usernames have to be unique.
String username = 'ProfileIdentityEventCondition@Test.com';
User user = new User(Alias = 'standt', Email='standarduser@testorg.com',
EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US',
LocaleSidKey='en_US', ProfileId = profileId,
TimeZoneSidKey='America/Los_Angeles', UserName=username);
return user;
}
}Behandeln Sie die beiden negativen Testfälle, indem Sie den Transaktionssicherheitsrichtlinien-Code aktualisieren, um beim Abfragen des Profilobjekts eine Überprüfung auf Ausnahmen oder Nullergebnisse vorzunehmen.
global class ProfileIdentityEventCondition implements TxnSecurity.EventCondition {
// For these powerful profiles, let's prompt users to complete 2FA
private Set<String> PROFILES_TO_MONITOR = new Set<String> {
'System Administrator',
'Custom Admin Profile'
};
public boolean evaluate(SObject event) {
try{
LoginEvent loginEvent = (LoginEvent) event;
String userId = loginEvent.UserId;
// get the Profile name from the current users profileId
Profile profile = [SELECT Name FROM Profile WHERE Id IN
(SELECT profileId FROM User WHERE Id = :userId)];
if (profile == null){
return false;
}
// check if the name of the Profile is one of the ones we want to monitor
if (PROFILES_TO_MONITOR.contains(profile.Name)) {
return true;
}
return false;
} catch(Exception ex){
System.debug('Exception: ' + ex);
return false;
}
}
}
