QA

Jak przyspieszyć testy jednostkowe za pomocą Stub API

testy jednostkowe

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

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:

  1. Tworzenia klasy testowej i/lub metody testowej.
  2. Tworzenia danych testowych.
  3. Wywoływania metody, którą chcemy przetestować.
  4. 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:

  1. Jak zbudować dane testowe bez wykorzystania DML?
  2. Co, jeśli logika obejmuje SOQL?
  3. 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.

baner

 

Ma ponad 7-letnie doświadczenie zawodowe. Od samego początku związany z technologią Salesforce. Uczestniczył w różnego rodzaju projektach – zarówno tych, które dostosowywały chmurę sprzedaży do potrzeb klienta, jak również w tworzeniu aplikacji ISV. Stara się rozwiązywać problemy i dostarczać rozwiązania, a nie kod. Gracz zespołowy z dobrymi umiejętnościami komunikacyjnymi.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/jak-przyspieszyc-testy-jednostkowe-za-pomoca-stub-api/" order_type="social" width="100%" count_of_comments="8" ]