Тесты для visalforce custom controller

Одна из самых неприятных вещей, с которомы сталкиваются новички на работе - это написание тестов. Что оно вообще такое и как их делать? В гугле есть какие то материалы, но с тестами на контроллер они как-то не вяжутся.

А почему собственно не вяжутся? Контроллер - это самый обычный Apex-класс, и имеет все свойства самого обычного класса, которые вы могли изучать на джаве, или на другом похожем языке. У него есть поля, есть методы, может быть конструктор. И тестировать его надо по большей части как самый обычный класс - к полям и методам контроллера можно обращаться так само, как к полям и методам любого класса.

Для примера я взяла код из трейлхеда Create & Use Custom Controllers

Когда вы пройдете этот модуль - у вас должен получиться вот такой код (единственное мое дополнение - аннотация @TestVisible

Апекс-класс:

public class ContactsListWithController {

    @TestVisible
    private String sortOrder = 'LastName';

    public List<Contact> getContacts() {
        List<Contact> results = Database.query(
            'SELECT Id, FirstName, LastName, Title, Email ' +
            'FROM Contact ' +
            'ORDER BY ' + sortOrder + ' ASC ' +
            'LIMIT 10'
        );
        return results;
    }


    public void sortByLastName() {
        this.sortOrder = 'LastName';
    }

    public void sortByFirstName() {
        this.sortOrder = 'FirstName';
    }

}

Вижуалфорс-пейдж:

<apex:page controller="ContactsListWithController">
    <apex:form>
        <apex:pageBlock title="Contacts List" id="contacts_list">
            <apex:pageBlockTable value="{! contacts }" var="ct">
                <apex:column value="{! ct.FirstName }">
                    <apex:facet name="header">
                        <apex:commandLink action="{! sortByFirstName }" reRender="contacts_list">
                            First Name
                        </apex:commandLink>
                    </apex:facet>
                </apex:column>
                <apex:column value="{! ct.LastName }">
                    <apex:facet name="header">
                        <apex:commandLink action="{! sortByLastName }" reRender="contacts_list">
                            Last Name
                        </apex:commandLink>
                    </apex:facet>
                </apex:column>
                <apex:column value="{! ct.Title }"/>
                <apex:column value="{! ct.Email }"/>
            </apex:pageBlockTable>
        </apex:pageBlock>
    </apex:form>
</apex:page>

В итоге страница должна выглядеть вот так, колонки First Name и Last Name должны быть кликабельны.

ab55baefb9a972ea06c57810ce063163_visualforce-custom-controllers-basic

И так, что мы имеем.

У нас есть класс ContactsListWithController, в нем есть приватное поле sortOrder, метод getContacts(), который вытягивает контакты, отсортированные по полю sortOrder, и два метода, sortByLastName() и sortByFirstName(), которые меняют sortOrder.

При загрузе страницы создается экземпляр класса-контроллера ContactsListWithController pageController = new ContactsListWithController() и страница дергает метод getContacts() у этого экземпляра (что бы записать полученные контакты в {! contacts } и отобразить их) - это будет первый тест.

Когда мы нажимаем на колонку First Name - вызывается метод sortByFirstName(), после чего срабатывает reRender="contacts_list", от чего перерисовывается id="contacts_list". То есть, после вызова sortByFirstName(), который приводит к изменению sortOrder страница снова обращается к методу getContacts() и получает контакты, отсортированные уже по новому ордеру, записывает их в {! contacts } и отображает.

Для апекса происходит вот что: вызывается sortByFirstName(), а потом getContacts() - это будет второй тест.

Вместо аннотации @TestVisible можно было сделать что бы поле sortOrder было public, а не private, но смысл в том, что это поле можно поменять только используя методы sortByLastName() и sortByFirstName(), если бы поле было паблик - то его можно было бы менять просто обратившись к нему. Аннотация нужна, что бы поле было видимое для тестов (если обратиться к нему вне теста - доступа не будет)

Что нужно, что бы написать тест на этот контроллер?

Для начала - созадть класс ContactsListWithControllerTest (ИмяТестируемогоКлассаTest) и пометить его аннотацией @IsTest

@IsTest
private class ContactsListWithControllerTest {
  
}

Этот класс может быть как public так и private, вот что говорится в документации:

Classes and methods defined as @isTest can be either private or public. The access level of test classes methods doesn’t matter. You need not add an access modifier when defining a test class or test methods. The default access level in Apex is private. The testing framework can always find the test methods and execute them, regardless of their access level.

Теперь нужно понять что мы тестируем. А что мы тестируем? Мы тестируем поведение вижуалфорс-страницы. Страница загружется, от этого создается экземпляр контроллера, потом у этого экземпляра вызывается метод getContacts()

Что бы это протестировать - нужно воспроизвести такое поведение, поэтому создаем тестовый метод и в нем воспроизводим:

@IsTest
static void testGetContacts() {

    ContactsListWithController pageController = new ContactsListWithController();
    List<Contact> contacts = pageController.getContacts();
    
}

Метод getContacts() возвращает лист контактов, мы записываем его в переменную, что бы потом проверять.

В тесты можно добавлять вызовы методов Test.startTest(); и Test.stopTest();

В одних источниках говорится что их можно добавлять не всегда, а только тогда, когда надо протестировать логику, связанную с лимитами (например, сколько раз запускался асинхронный процесс или сколько раз отправлялись письма) в других же говорится что надо всегда указывать начало и конец теста, это нужно больше для других программистов, что бы было понятно что происходит.

  • Сначала идет подготовка данных перед тестом.

  • Потом Test.startTest();

  • Потом собственно вызов тестируемого метода.

  • Потом Test.stopTest();

  • И в конце проверки System.assertEquals();

Проверки не обязательно идут только в конце, они могут быть как в начале, перед тестом (что бы убедиться что изначально данные были одни, а изменились они именно после вызова тестируемого метода) так и между Test.startTest(); и Test.stopTest();
Но без прямой на это необходимости так не делают, по возможности выносят их в конец.

Что же сравнивать в нашем случае? Мы получаем лист контактов, но он будет пустой (тесты не имеют доступа к записям орга) поэтому нужно сначала добавить в бд контакты, которые потом вытянутся в методе getContacts(). А в конце надо проверить полученный лист контактов - в данном случае мы можем проверить размер листа, а так же имена и фамилии контактов, попавших в лист. Создавать нужно несколько рекордов, потому что один рекорд не даст понять отсортировалось оно или нет. Дополнительно можно проверить что поле sortOrder инициализоровалось так, как задумано, и поэтому контакты отсортированы именно так.

@IsTest
static void testGetContacts() {

    /*два контакта, которые будут идти в разном порядке при сортироваке 
      по имени и фамилии*/

    Contact contact1 = new Contact(FirstName = 'Adam', LastName = 'Smith');
    insert contact1;

    Contact contact2 = new Contact(FirstName = 'Steve', LastName = 'Apple');
    insert contact2;

    Test.startTest();

    ContactsListWithController pageController = new ContactsListWithController();
    List<Contact> contacts = pageController.getContacts();

    Test.stopTest();

    System.assertEquals(2, contacts.size());
    System.assertEquals('Steve', contacts.get(0).FirstName);
    System.assertEquals('Apple', contacts.get(0).LastName);
    System.assertEquals('Adam', contacts.get(1).FirstName);
    System.assertEquals('Smith', contacts.get(1).LastName);

    System.assertEquals('LastName', pageController.sortOrder);

}

Не всегда проверять каждый рекорд - хорошая идея, но в данном случае мы убеждаемся что контакты идут в нужном порядке. Можно было вытянуть контакты SOQL-запросом и сравнить контакты в листе, который вытянули в запросе, и в листе, который получили из контролера.

@IsTest
static void testGetContacts() {

    /*два контакта, которые будут идти в разном порядке при сортироваке 
      по имени и фамилии*/

    Contact contact1 = new Contact(FirstName = 'Adam', LastName = 'Smith');
    insert contact1;

    Contact contact2 = new Contact(FirstName = 'Steve', LastName = 'Apple');
    insert contact2;

    Test.startTest();

    ContactsListWithController pageController = new ContactsListWithController();
    List<Contact> contacts = pageController.getContacts();

    Test.stopTest();

    System.assertEquals(2, contacts.size());

    List<Contact> allContacts = [SELECT FirstName, LastName, Title, Email 
                                    FROM Contact ORDER BY LastName ASC];

    for (Integer i = 0; i < allContacts.size(); i++) {
        System.assertEquals(contacts.get(i).FirstName, allContacts.get(i).FirstName);
        System.assertEquals(contacts.get(i).LastName, allContacts.get(i).LastName);
    }

    System.assertEquals('LastName', pageController.sortOrder);

}

Или еще проще - просто стравнить два листа, заменить цикл вот такой строкой:

    System.assertEquals(contacts, allContacts);

Как именно проверять данные зависит от того, что именно нужно проверить, в данном случае лучше всего подошел бы последний вариант, но и предыдущие варианты допустимы.

Дальше нужно протестировать логику, когда sortOrder меняется, после чего метод getContacts() возвращает контакты уже в другом порядке. Так само - сначала создаем тестовые данные, потом запускаем тестируемые методы, потом проверяем:

@IsTest
static void testGetContactsFirstNameOrder() {

    Contact contact1 = new Contact(FirstName = 'Adam', LastName = 'Smith');
    insert contact1;

    Contact contact2 = new Contact(FirstName = 'Steve', LastName = 'Apple');
    insert contact2;

    Test.startTest();

    ContactsListWithController pageController = new ContactsListWithController();
    pageController.sortByFirstName();
    List<Contact> contacts = pageController.getContacts();

    Test.stopTest();

    System.assertEquals('FirstName', pageController.sortOrder);

    System.assertEquals(2, contacts.size());

    List<Contact> allContacts = [SELECT FirstName, LastName, Title, Email 
                                    FROM Contact ORDER BY FirstName ASC];

    System.assertEquals(contacts, allContacts);

}

Точно так же нужно протестировать метод sortByLastName(); - проверить что изменилось поле sortOrder и что getContacts() теперь возвращает другой результат.

Еще можно добавить тест на Exception. Мы не можем сделать так, что бы система бросила QueryException при правильно введенных данных, и при этом мы не можем гарантировать что такого никогда не случится. Поэтому, по хорошему, по правильному, эксепшены надо перехватывать и обрабатывать, и писать тест на то, что эксепшен перехватился, обработался, и все это привело к тому результату, который мы и планировали получить в случае эксепшена. Но тесты на эксепшены относятся вообще к тестам, а не только к вижуалфорсу, так что об этом я потом сделаю отдельный материал.

Теперь сделаем то, что нужно было сделать с самого начала - вынесем создание тестовых данных в @TestSetup

@TestSetup static void setup() {

    Contact contact1 = new Contact(FirstName = 'Adam', LastName = 'Smith');
    insert contact1;

    Contact contact2 = new Contact(FirstName = 'Steve', LastName = 'Apple');
    insert contact2;
    
}

@IsTest
static void testGetContacts() {

    Test.startTest();

    ContactsListWithController pageController = new ContactsListWithController();
    List<Contact> contacts = pageController.getContacts();

    Test.stopTest();

    System.assertEquals(2, contacts.size());

    ...

Теперь перед Test.startTest(); не нужно создавать контакты, ведь то, что создается в сетапе - создается для каждого теста).

А еще, создание тестовых данных можно вынести в отдельный класс TestDataFactory (название может отличаться, главное что бы смысл был тот же)

Делатей может быть еще больше, зависимо от тонкостей что именно тестируется, какие стандарты в компании, и просто от личного мнения, основаного на личном опыте.

Контроллеры для лайтнинг-компонентов пишутся по другому, там экземпляр контроллера не создается и все методы статичны, и в какой то степени с лайтнингом проще (но тут уж все фломастеры разные)

Пока это все, но я периодически дополняю)

Ставьте лайки, пишите комментарии, следите за обновлениями :wink:
:cloud: :cloud: :cloud: :cloud: :cloud: :cloud: :cloud: :cloud: :cloud: :cloud: :cloud: :cloud:

6 Likes