Вы находитесь здесь:
Тестирование Apex расширенной политики безопасности транзакций
Написание устойчивых тестов рекомендуется в программотехнике для обеспечения ожидаемого алгоритма кода и определения ошибок до того, как это сделают ваши пользователи или клиенты. Создание тестов для кода Apex политики безопасности транзакций еще более важно, поскольку он выполняется во время важных действий пользователя в организации Salesforce. Например, ошибка политики LoginEvent, не обнаруженная тестом, может привести к блокировке пользователей организации, а этой ситуации нужно избежать.
Требуемые версии
| Доступно в версиях Salesforce Classic (недоступно во всех организациях) и Lightning Experience. |
Доступно в версиях: Enterprise Edition, Unlimited Edition и Developer Edition. Требуется приобретение подписок на дополнительные компоненты Salesforce Shield или Salesforce Event Monitoring. |
При тестировании кода Apex посредством симуляции набора условий вы по определению создаете поблочные тесты. Но написания поблочных тестов недостаточно. Проконсультируйтесь с рабочими бизнес группами и группами по безопасности, чтобы определить все способы использования. Потом создайте универсальный план теста, которые воссоздает фактическую работу пользователей, используя данные теста в безопасной среде sandbox. План теста обычно содержит как тестирование вручную, так и автоматическое с использованием внешних инструментов, например, Selenium.
Рассмотрим несколько поблочных тестов. Ниже указана политика Apex, которую необходимо протестировать.
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;
}
}
Планирование и написание тестов
Прежде чем писать тесты, нужно определить положительные и отрицательные способы использования, содержащиеся в нашем плане теста.
Если метод 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
|
Если метод evaluate получает... |
И... | Потом метод evaluate возвращает... |
|---|---|---|
| Объект LoginEvent | (условия отсутствуют) |
false
|
| Нулевое значение | (условия отсутствуют) |
false
|
| Объект ApiEvent | Поле QueriedEntities нулевое |
false
|
| Объект ReportEvent | Поле RowsProcessed нулевое |
false
|
Ниже указан код тестирования Apex, внедряющий все эти случаи использования.
/**
* 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));
}
}Настройка кода политики после выполнения тестов
Например, вы выполните тесты, а тестовый сценарий testNullQueriedEntities не выполнит System.NullPointerException: Attempt to de-reference a null
object ошибки. Отличная новость, тесты определили область политики безопасности транзакций, которая не проверяет неожиданные или нулевые значения. Поскольку политики выполняются во время важных операций организации, обеспечьте наименее проблемный сбой политик, чтобы они не блокировали важные функции при возникновении ошибки.
Ниже указан способ обновления метода evaluate в классе Apex для грациозной обработки нулевых значений.
private boolean evaluate(String queriedEntities, Decimal rowsProcessed) {
boolean containsLead = queriedEntities != null ? queriedEntities.contains('Lead') : false;
if (containsLead && rowsProcessed > 2000){
return true;
}
return false;
}Мы изменили код, чтобы перед выполнением операции .contains над переменной queriedEntities сначала проверить, является ли значение нулевым. Это изменение обеспечивает отсутствие обратной ссылки а нулевой объект.
В общем, при наступлении неожиданных значений или ситуаций в коде Apex, у вас есть два варианта: Определите наиболее приемлемый вариант для ваших пользователей.
- Пропустите значения или ситуацию и верните
false, чтобы политика не сработала. - Ошибка закрытия операции посредством возврата
true.
Расширенный пример
Ниже указана более сложная политика Apex, использующая запросы SOQL для получения профиля пользователя, пытающегося войти в систему.
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;
}
}Ниже указан наш план тестирования для положительных тестовых случаев:
-
- Если у пользователя, пытающегося войти в систему, есть профиль, который мы хотим отслеживать, метод
evaluateвозвращаетtrue. - Если пользователь, пытающийся войти в систему, не имеет нужного профиля, метод
evaluateвозвращаетfalse.
- Если у пользователя, пытающегося войти в систему, есть профиль, который мы хотим отслеживать, метод
И вот наш план для отрицательных тестовых случаев:
-
- Если запрос объекта «Профиль» возвращает исключение, метод
evaluateвозвращаетfalse. - Если запрос объекта «Профиль» возвращает значение null, то метод
evaluateвозвращает значениеfalse.
- Если запрос объекта «Профиль» возвращает исключение, метод
Поскольку каждый пользователь Salesforce всегда имеет назначенный профиль, не необходимости создавать отрицательный тест. Также невозможно создать фактические тесты для двух отрицательных способов использования. Мы сами займемся ими при обновлении самой политики. Но мы четко указываем случаи использования в своем плане, чтобы обязательно охватить много разных ситуаций.
Положительные способы тестирования зависят от результатов запросов SQQL. Чтобы обеспечить корректное выполнение тестирования, необходимо также создать данные тестов. Рассмотрим код тестирования.
/**
* 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;
}
}Обработаем два отрицательных способа тестирования, обновив код политики безопасности транзакций для проверки наличия исключений или нулевых результатов при запросе объекта «Профиль».
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;
}
}
}
