Jak przyspieszyć testy jednostkowe za pomocą Stub API
Jeśli kiedykolwiek mieliście okazję pracować z bazą kodu zawierającą więcej niż dziesięć klas, problemy związane z czasem wykonania Testów Jednostkowych są Wam zapewne dobrze znane. Choć ogólne działanie całego systemu musi i będzie się stopniowo poprawiać, w tym artykule spróbuję odpowiedzieć na pytanie, co my jako programiści możemy zrobić sami, żeby usprawnić swoją pracę.
Chcąc lepiej zrozumieć ten problem, posłużymy się przykładem. W tym celu utworzę metodę wywołaną w kontekście „before insert”, w triggerze podczas zapisu rekordu typu Contact. Jej głównym celem będzie ustawienie kilku pól w oparciu o nadrzędny rekord Account.
public with sharing class PropagateAccountDataForNewContacts(){
public void propagateAccountDataToChildContact(List<Contact> newContacts){
Set<Id> parentAccountIds = new Set<Id>();
for(Contact newContact : newContacts){
parentAccountIds.add(newContact.AccountId);
}
Map<Id, Account> parentAccountsMap = new Map<Id, Account>
([SELECT Id, Phone, ...,
SomeExtraField__c FROM Account WHERE Id IN :parentAccountIds]);
for(Contact newContact : newContacts){
setFieldsForNewContact(newContact, parentAccountsMap.get(newContact.AccountId));
}
}
}
Spis treści
Test Jednostkowy
Zadaniem Testów Jednostkowych jest zagwarantowanie braku błędów w kodzie – poprzez sprawdzanie, czy części kodu działają bez problemów. W celu wdrożenia kodu w środowisku produkcyjnym, Testy Jednostkowe powinny objąć przynajmniej 75 procent kodu. Zazwyczaj proces tworzenia nowego Testu Jednostkowego jest dość prosty i składa się z następujących etapów:
- Tworzenia klasy testowej i/lub metody testowej.
- Tworzenia danych testowych.
- Wywoływania metody, którą chcemy przetestować.
- Oceny wyników.
Takie podejście dobrze się sprawdza w zarządzaniu małymi i średnimi projektami, ale może spowodować wiele problemów, gdy zwiększy się baza kodu. Standardowy Test Jednostkowy jest dość prosty w użyciu w następującym przykładzie:
@isTest
static void testShouldPropagateAccountDataToNewContacts(){
Account a = TestUtil.createAccount();
insert a;
Contact testContact = TestUtil.createContact(a.Id);
Test.startTest();
insert testContact;
Test.stopTest();
Contact resultContact = TestUtil.getContactById(testContact.Id);
System.assertEquals(a.Phone, resultContact.Phone,
'Phone field should propagate from Account to Contact record.');
System.assertEquals(a.SomeExtraField__c, resultContact.SomeExtraField__c,
'SomeExtraField field should propagate from Account to Contact record.');
}
Problemy z Testami Jednostkowymi
Każdy, kto stosuje w projekcie CI, dobrze wie, ile czasu potrzeba na wdrożenie produkcyjne. Czas wykonania wymagany w Testach Jednostkowych staje się problemem, zwłaszcza w przypadku większych zespołów – ze względu na duże wykorzystanie bazy danych.
W Salesforce tworzenie Testów Jednostkowych, które są faktycznie Testami Jednostkowymi, nie jest łatwe. W standardowym podejściu, zamiast Testów Jednostkowych, tworzone są Testy Systemowe. Ich głównym celem jest sprawdzenie ogólnej logiki, bez dzielenia kodu na wiele mniejszych części. W celu uzyskania faktycznych Testów Jednostkowych i skrócenia czasu wykonania testu, musimy zmienić sposób myślenia. Z testem w podanym przykładzie nie ma problemu, o ile test ten nie jest wykonywany na środowisku ze „154” deklaratywnymi narzędziami, które pracują z obiektem Contact.
Nowe podejście
Jeśli Test Jednostkowy ma zostać uznany za prawdziwy, musi zostać zbudowana warstwa dostępu do danych tylko do komunikowania się z bazą danych i zainicjowania nowego sposobu wykonywania testów – wszystko w pamięci. Pojawiają się przy tym następujące pytania:
- Jak zbudować dane testowe bez wykorzystania DML?
- Co, jeśli logika obejmuje SOQL?
- Jak ta dodatkowa warstwa wpłynie na pracę programistów oraz utrzymanie kodu?
Najważniejszym polem zapisu jest ID i jest to jeden z kluczowych powodów, dla których wykorzystujemy DML podczas konfiguracji testów. W budowaniu odpowiednich Testów Jednostkowych, baza danych nie jest wymagana do uzyskania rekordu z ID. Zamiast tego, można utworzyć rekord z ID.
W celu utworzenia ID zapisu, wymagany jest unikalny prefiks SObject, inaczej zostanie wygenerowany błąd. W celu odebrania odpowiedniego prefiksu, należy zastosować metodę klasy Schema:
sObjectType.getDescribe ().getKeyPrefix (), gdzie sObjectType może być pobrany z globalnego opisu lub z samej klasy SObject – Account. SObjectType.
Do tego celu może być wykorzystana prosta funkcja:
public static String getFakeId(Schema.SObjectType sObjectType){
String result = String.valueOf(fakeIdNumber++);
return sObjectType.getDescribe().getKeyPrefix() + '0'.repeat(12-result.length())
+ result;
}
Po utworzeniu rekordu z ID, ID może być wykorzystany w relacji typu lookup lub relacji typu master-detail – w celu utworzenia struktur danych wymaganych do wykonywania testów.
Wszelkie SOQL zawarte w logice powinny być przeniesione do odrębnej klasy w nowej warstwie, o nazwie Data Access Layer. Te typy klas będą naszym punktem kontaktu z bazą danych. Bazując na poniższym przykładzie, zamiast:
for(Contact contact : [SELECT Id, Name FROM Contact WHERE Id IN :contactIds]){
Dostuff(contact);
}
Ta dodatkowa warstwa zapewnia ekstra korzyść. Po pierwsze, łatwiej jest utrzymywać pojedynczy punkt kontaktu z bazą danych niż mieć zapytania rozsiane w całym kodzie. Po drugie, opisowe metody, które będą wyszukiwać dane, mogą być uproszczone, ułatwiając naukę osobom pracującym z tym kodem w przyszłości. Po trzecie wreszcie, istnieje możliwość symulowania wyników wyszukiwania w Testach Jednostkowych – tak, że logika biznesowa może zostać przetestowana kompleksowo bez większego wpływu na wykorzystanie zasobów.
W podanym przykładzie można zastosować prostą klasę, na przykład:
public with sharing class AccountDataAccess {
public List<Account> getAccountsForGivenIds(Set<Id> accountIds){
if(accountIds.isEmpty()){
return [SELECT Id, Phone, ...,
SomeExtraField__c FROM Account WHERE Id IN :accountIds];
}
else
{
return new List<Account>();
}
}
}
Stub API
Chcąc przyspieszyć wykonanie Testów Jednostkowych za pomocą Stub API, należy zbudować symulacyjną strukturę ramową w celu ‘zasymulowania’ bazy danych. Mogą już istnieć testy wykorzystujące technikę symulowania wyników, ponieważ jest to standardowy sposób testowania kodu, który wykorzystuje wywołania do zewnętrznych systemów.
Ogólna koncepcja jest podobna: symulowane wyniki innych zewnętrznych systemów są wykorzystywane w celu sprawdzenia oczekiwanych wyników względem logiki biznesowej. W tym scenariuszu systemem zewnętrznym jest baza danych. W ramach przygotowania tworzonych jest kilka klas. Pierwszą będzie MockProvider – ta klasa umożliwia użytkownikom symulowanie wszystkich metod w symulowanej klasie poprzez mapowanie.
@isTest
public class MockProvider implements System.StubProvider {
private Map<String, Object> stubbedMethodMap;
public MockProvider(Map<String, Object> stubbedMethodMap) {
this.stubbedMethodMap = stubbedMethodMap;
}
public Object handleMethodCall(Object stubbedObject, String stubbedMethodName,
Type returnType, List<Type> listOfParamTypes,
List<String> listOfParamNames, List<Object> listOfArgs) {
Object result;
if (stubbedMethodMap.containsKey(stubbedMethodName)) {
result = stubbedMethodMap.get(stubbedMethodName);
}
return result;
}
}
Następnym krokiem jest utworzenie klasy MockService, która będzie odpowiedzialna za symulowane wyniki funkcji w następujący sposób:
public class MockService {
private MockService() {}
public static MockProvider getInstance(Map<String, Object> stubbedMethodMap) {
return new MockProvider (stubbedMethodMap);
}
public static Object createMock(Type typeToMock, Map<String, Object> stubbedMethodMap) {
return Test.createStub(typeToMock, MockService.getInstance(stubbedMethodMap));
}
}
Jak widać, funkcja createMock jest wykorzystywana do utworzenia obiektu pozornego. Wykorzystanie Test.createStub wywołuje Stub API – system jest informowany, która klasa zostanie ‘zasymulowana’ oraz co zostanie zwrócone w przypadku określonych wywołań metod. Wykorzystanie tej usługi jest bardzo proste:
ClassToTest.dataAccessLayerClassInstance = (DataAccessLayerClass)
MockService.createMock(DataAccessLayerClass.class, new Map<String, Object>{
'getRecords' => resultForGetRecords,
'getChildRecords' => resultForGetChildRecords,
'updateRecords' => null
});
Po wywołaniu metody getRecords () z dataAccessLayerClassInstance w ramach testowanej klasy, rezultatem będzie resultForGetRecords.
We wspomnianym przykładzie muszą zostać dokonane pewne drobne korekty – wszystko w celu wykorzystania Stub API w testach. Zgodnie z powyższym należy zastosować klasę Data Access Layer zamiast wbudowanych SOQL.
public with sharing class PropagateAccountDataForNewContacts(){
@TestVisible private AccountDataAccess accountData = new AccountDataAccess();
public void propagateAccountDataToChildContact(List<Contact> newContacts){
Set<Id> parentAccountIds = new Set<Id>();
for(Contact newContact : newContacts){
parentAccountIds.add(newContact.AccountId);
}
Map<Id, Account> parentAccountsMap = new Map<Id, Account>
(accountData.getAccountsForGivenIds(parentAccountIds));
for(Contact newContact : newContacts){
setFieldsForNewContact(newContact, parentAccountsMap.get(newContact.AccountId));
}
}
}
Po wprowadzeniu zmian, wystarczy użyć klasy MockService w klasie Testu Jednostkowego i uzyskać szybkie wyniki, bez dotykania bazy danych.
@isTest
static void testShouldPropagateAccountDataToNewContacts(){
Account mockedAccount = new Account(
Id = TestUtil.getFakeId(Account.SObjectType),
Name = 'Acme',
Phone = TestUtil.generatePhone(),
SomeExtraField__c = 'extraValue'
);
Contact testContact = new Contact(
Id = TestUtil.getFakeId(Contact.SObjectType);
FirstName = 'John',
LastName = 'Doe'
);
PropagateAccountDataForNewContacts testedClass = new PropagateAccountDataForNewContacts();
testedClass.accountData = (AccountDataAccess) MockService.createMock
(AccountDataAccess.class, new Map<String, Object>
{
'getAccountsForGivenIds' => new List<Account>{mockedAccount}
});
Test.startTest();
PropagateAccountDataForNewContacts.propagateAccountDataToChildContact
(new List<Contact>{testContact};
Test.stopTest();
System.assertEquals(mockedAccount.Phone, testedClass.Phone,
'Phone field should propagate from Account to Contact record.');
System.assertEquals(mockedAccount.SomeExtraField__c, testedClass.SomeExtraField__c,
'SomeExtraField field should propagate from Account to Contact record.');
}
Podsumowanie
Choć czas wykonania Testów Jednostkowych może być problematyczny, to dzięki zaprezentowanej w artykule metodzie, można znacząco przyspieszyć proces, a dzięki temu usprawnić swoją codzienną pracę.
Artykuł został pierwotnie opublikowany na stronie softserveinc.com. Zdjęcie główne artykułu pochodzi z unsplash.com.
Podobne artykuły
Jak budować efektywną strategię QA i usprawnić współpracę na linii Dev-QA
Czy QA to nadal drzwi do IT i co rynek „gotuje” testerom? Wywiad z Jakubem Klechem
Pair testing: jak developerzy i testerzy wspólnie dbają o jakość
Jako twórcy aplikacji mało wiemy o odbiorcach. O użyteczności i dostępności w IT
Klienci chcą rozwiązań problemów, a nie fajerwerków. O zjawisku overengineeringu
Zmienił się apetyt na ryzyko. Organizacje w końcu kładą nacisk na budowę kultury jakości
Automatyzuj przewidywalną część pracy. Zaoszczędzony czas poświęć na dogłębną analizę kodu

