Регистрация  |  Вход

Test.createStub

Хочется написать красивый тест. Пытаюсь использовать Test.createStub. Может у кого есть нормальный пример, а не тот непонятный ужас из справки.

Спасибо.

Хочется написать красивый тест. Пытаюсь использовать Test.createStub. Может у кого есть [i]нормальный[/i] пример, а не тот непонятный ужас из [url=https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_stub_api.htm]справки[/url].

Спасибо.

Глянул документацию, но не совсем уловил изюминку данной фичи. Можешь по простому рассказать нафига данная штука? Может на реальном примере

Глянул документацию, но не совсем уловил изюминку данной фичи. Можешь по простому рассказать нафига данная штука? Может на реальном примере :) 

micha_s
Хочется написать красивый тест.

Красивый это тот, который понятен всем по уровню сложности и аккуратно оформлен для читабельности.
Хорошо - когда сеньор написал код, а любой джуниор его понял.
Плохо - когда джуниор написал код, а понять его смог только хороший сеньор.

Даже Salesforce предупреждает, что эта фича предполагает продвинутый уровень владения Apex и глубокое понимание юнит тестирования и mock фрэймворков.

This feature is intended for advanced Apex developers. Using it requires a thorough understanding of unit testing and mocking frameworks. If you think that a mocking framework is something that makes fun of you, you might want to do a little more research before reading further.

micha_s
Может у кого есть нормальный пример, а не тот непонятный ужас из справки.

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

Dmitry Shnyrev
Глянул документацию, но не совсем уловил изюминку данной фичи. Можешь по простому рассказать нафига данная штука? Может на реальном примере

Ну, к примеру, нам не нужно использовать конструкции типа Test.isRunningTest() или передавать лишние параметры в методы только ради особого тестового сценария выполнения. Мы мокаем(в коде из справки) DateHelper класс и возвращаем(в MockProvider.handleMethodCall() методе) нужное нам значение даты(а не текущее, как это делает реальный метод Date.today().format()), если метод возвращает тип String.
Код в справке максимально упростили. Там вообще желательно было бы использовать не возвращаемый тип, а имя метода да и без такого харкода делать.
п.с. я ещё не пользовался этим stub фреймворком, т.к. просто не было времени раньше на проектах(приходилось фичи пилить, а не тесты оптимизировать), но сейчас с радостью обсужу со всеми, раз уже есть время.

[quote="micha_s"]Хочется написать красивый тест.[/quote]

Красивый это тот, который понятен всем по уровню сложности и аккуратно оформлен для читабельности.
Хорошо - когда сеньор написал код, а любой джуниор его понял.
Плохо - когда джуниор написал код, а понять его смог только хороший сеньор.

Даже Salesforce предупреждает, что эта фича предполагает продвинутый уровень владения Apex и глубокое понимание юнит тестирования и mock фрэймворков.
[quote]
This feature is intended for advanced Apex developers. Using it requires a thorough understanding of unit testing and mocking frameworks. If you think that a mocking framework is something that makes fun of you, you might want to do a little more research before reading further.
[/quote]

[quote="micha_s"]Может у кого есть нормальный пример, а не тот непонятный ужас из справки.[/quote]

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

[quote="Dmitry Shnyrev"]Глянул документацию, но не совсем уловил изюминку данной фичи. Можешь по простому рассказать нафига данная штука? Может на реальном примере [/quote]

Ну, к примеру, нам не нужно использовать конструкции типа Test.isRunningTest() или передавать лишние параметры в методы только ради особого тестового сценария выполнения. Мы мокаем(в коде из справки) DateHelper класс и возвращаем(в MockProvider.handleMethodCall() методе) нужное нам значение даты(а не текущее, как это делает реальный метод Date.today().format()), если метод возвращает тип String.
Код в справке максимально упростили. Там вообще желательно было бы использовать не возвращаемый тип, а имя метода да и без такого харкода делать.
п.с. я ещё не пользовался этим stub фреймворком, т.к. просто не было времени раньше на проектах(приходилось фичи пилить, а не тесты оптимизировать), но сейчас с радостью обсужу со всеми, раз уже есть время.

micha_s
Хочется написать красивый тест. Пытаюсь использовать Test.createStub. Может у кого есть нормальный пример, а не тот непонятный ужас из справки.

Спасибо.

Уже года 3 как использую свой фреймворк для тестов. В последнее время добавил стабы. Все тестовые данные храняться в одном или нескольких статик ресурсах и в тестах подгружаются автоматически.

[quote="micha_s"]Хочется написать красивый тест. Пытаюсь использовать Test.createStub. Может у кого есть нормальный пример, а не тот непонятный ужас из справки.

Спасибо.[/quote]

Уже года 3 как использую свой фреймворк для тестов. В последнее время добавил стабы. Все тестовые данные храняться в одном или нескольких статик ресурсах и в тестах подгружаются автоматически.

Мой случай:

Дано:


  1. Очень старый проект с Force.com Sites.
  2. Кастомная Login страница.
  3. Site.login в тестах
  4. всегда вернёт null.

Требуется: Протестировать как нормальный логин, так и неправильный. Минимальные изменения в тестируемый код.

Решение:

Очень прошу отписаться по теме, на сколько мой код читабельный и правильно ли я понял идею этого API.
Заранее благодарен.

global without sharing class LoginForm_Ctrl {

public Boolean loginFailed { get; private set; }

public String username { get; set; }
public String password { get; set; }

public LoginForm_Ctrl() {}

public PageReference login() {
PageReference page = stub.site_login(username, password, '/');
loginFailed = (page == null);
return page;
}


@TestVisible
LoginForm stub = this;

public PageReference site_login(String username, String password, String startUrl) {
return Site.login(username, password, startUrl);
}

}

@IsTest
global class LoginForm_Test implements StubProvider {

@IsTest
static void login_behaviour_test() {
LoginForm ctrl;

LoginForm ss_loginOk = (LoginForm) Test.createStub(LoginForm.class, new LoginForm_Test(LoginFormStates.LOGIN_OK, '/'));
LoginForm ss_loginKo = (LoginForm) Test.createStub(LoginForm.class, new LoginForm_Test(LoginFormStates.LOGIN_KO, null));

Test.setCurrentPage(Page.LoginForm);

System.runAs([SELECT Id FROM User WHERE CommunityNickname = 'SiteName' LIMIT 1][0]) {
Test.startTest();
{
ctrl = new LoginForm();

ctrl.username = 'aaa';
ctrl.password = 'aaa';

ctrl.stub = ss_loginOk;
System.assertEquals('/', ctrl.login().getUrl());
System.assert(!ctrl.loginFailed);

ctrl.stub = ss_loginKo;
System.assertEquals(null, ctrl.login());
System.assert(ctrl.loginFailed);
}
Test.stopTest();
}
}


enum LoginFormStates { LOGIN_OK, LOGIN_KO }
LoginFormStates lsf;

String refUrl;

LoginForm_Test(LoginFormStates lsf, String refUrl) {
this.lsf = lsf;
this.refUrl = refUrl;
}

public Object handleMethodCall(
Object stubbedObject, String stubbedMethodName, Type returnType, List<Type> listOfParamTypes,
List<String> listOfParamNames, List<Object> listOfArgs
) {
if ('site_login'.equals(stubbedMethodName)) {
return LoginFormStates.LOGIN_OK.equals(lsf) ? new PageReference(refUrl) : null;
}

return null;
}

}

Мой случай:

Дано:
[list=1]
[*] Очень старый проект с Force.com Sites.
[*] Кастомная Login страница.
[*] Site.login в тестах [i]всегда[/i] вернёт null.
[/list]

Требуется: Протестировать как нормальный логин, так и неправильный. Минимальные изменения в тестируемый код.

Решение:  

  Очень прошу отписаться по теме, на сколько мой код читабельный и правильно ли я понял идею этого API.
Заранее благодарен.

[code]global without sharing class LoginForm_Ctrl {

    public Boolean loginFailed { get; private set; }

    public String username { get; set; }
    public String password { get; set; }

    public LoginForm_Ctrl() {}

    public PageReference login() {
        PageReference page = stub.site_login(username, password, '/');
        loginFailed = (page == null);
        return page;
    }


    @TestVisible
    LoginForm stub = this;

    public PageReference site_login(String username, String password, String startUrl) {
        return Site.login(username, password, startUrl);
    }

}[/code]



[code]@IsTest
global class LoginForm_Test implements StubProvider {

    @IsTest
    static void login_behaviour_test() {
        LoginForm ctrl;

        LoginForm ss_loginOk = (LoginForm) Test.createStub(LoginForm.class, new LoginForm_Test(LoginFormStates.LOGIN_OK, '/'));
        LoginForm ss_loginKo = (LoginForm) Test.createStub(LoginForm.class, new LoginForm_Test(LoginFormStates.LOGIN_KO, null));

        Test.setCurrentPage(Page.LoginForm);

        System.runAs([SELECT Id FROM User WHERE CommunityNickname = 'SiteName' LIMIT 1][0]) {
            Test.startTest();
            {
                ctrl = new LoginForm();

                ctrl.username = 'aaa';
                ctrl.password = 'aaa';

                ctrl.stub = ss_loginOk;
                System.assertEquals('/', ctrl.login().getUrl());
                System.assert(!ctrl.loginFailed);

                ctrl.stub = ss_loginKo;
                System.assertEquals(null, ctrl.login());
                System.assert(ctrl.loginFailed);
            }
            Test.stopTest();
        }
    }


    enum LoginFormStates { LOGIN_OK, LOGIN_KO }
    LoginFormStates lsf;

    String refUrl;

    LoginForm_Test(LoginFormStates lsf, String refUrl) {
        this.lsf = lsf;
        this.refUrl = refUrl;
    }

    public Object handleMethodCall(
            Object stubbedObject, String stubbedMethodName, Type returnType, List<Type> listOfParamTypes,
            List<String> listOfParamNames, List<Object> listOfArgs
    ) {
        if ('site_login'.equals(stubbedMethodName)) {
            return LoginFormStates.LOGIN_OK.equals(lsf) ? new PageReference(refUrl) : null;
        }

        return null;
    }
    
}[/code]


public without sharing class LoginFormHelper {

public enum State { LOGIN_OK, LOGIN_KO }

public PageReference siteLogin(String username, String password, String startUrl) {
return Site.login(username, password, startUrl);
}

}

public without sharing class LoginFormCtrl {

public Boolean loginFailed { get; private set; }
public String username { get; set; }
public String password { get; set; }

@TestVisible
private LoginFormHelper helper;

public LoginFormCtrl() {
helper = new LoginFormHelper();
}

public PageReference login() {
PageReference page = helper.siteLogin(username, password, '/');

loginFailed = page == null;

return page;
}

}

@isTest
public class LoginFormHelperMock implements StubProvider {

private LoginFormHelper.State state;
private String refUrl;

public LoginFormCtrlMock(LoginFormHelper.State state, String refUrl) {
this.state = state;
this.refUrl = refUrl;
}

public Object handleMethodCall(Object stubbedObject, String stubbedMethodName,
Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames,
List<Object> listOfArgs) {
Object returnValue;

if ('site_login'.equals(stubbedMethodName)) {
returnValue = LoginFormHelper.State.LOGIN_OK == state
? new PageReference(refUrl)
: null;
}

return returnValue;
}
}

@isTest
private class LoginFormCtrlTest {

@isTest
private static void testLoginOk() {
Test.setCurrentPage(Page.LoginForm);

LoginFormCtrl ctrl = new LoginFormCtrl();
ctrl.helper = getHelperMock(LoginFormHelper.State.LOGIN_OK, '/');

User communityUser = getCommunityUser();

System.runAs(communityUser) {
Test.startTest();

ctrl.username = 'aaa';
ctrl.password = 'aaa';

System.assertEquals('/', ctrl.login().getUrl());
System.assert(!ctrl.loginFailed);

Test.stopTest();
}
}

@isTest
private static void testLoginKo() {
Test.setCurrentPage(Page.LoginForm);

LoginFormCtrl ctrl = new LoginFormCtrl();
ctrl.helper = getHelperMock(LoginFormHelper.State.LOGIN_KO, null);

User communityUser = getCommunityUser();

System.runAs(communityUser) {
Test.startTest();

ctrl.username = 'aaa';
ctrl.password = 'aaa';

System.assertEquals(null, ctrl.login());
System.assert(ctrl.loginFailed);

Test.stopTest();
}
}

private static LoginFormHelper getHelperMock(LoginFormHelper.State state, String refUrl) {
LoginFormHelper helperMock = (LoginFormHelper) Test.createStub(
LoginFormHelper.class,
new LoginFormHelperMock(state, refUrl)
);

return helperMock;
}

private static User getCommunityUser() {
User communityUser = [SELECT Id FROM User WHERE CommunityNickname = 'SiteName' LIMIT 1][0];

return communityUser;
}

}

Писал код прямо в редакторе сообщения. Если будут ошибки, то пиши

[code]
    public without sharing class LoginFormHelper {

        public enum State { LOGIN_OK, LOGIN_KO }

        public PageReference siteLogin(String username, String password, String startUrl) {
            return Site.login(username, password, startUrl);
        }

    }
[/code]

[code]
    public without sharing class LoginFormCtrl {

        public Boolean loginFailed { get; private set; }
        public String username { get; set; }
        public String password { get; set; }

        @TestVisible
        private LoginFormHelper helper;

        public LoginFormCtrl() {
            helper = new LoginFormHelper();
        }
     
        public PageReference login() {
            PageReference page = helper.siteLogin(username, password, '/');

            loginFailed = page == null;

            return page;
        }

    }
[/code]

[code]
@isTest
public class LoginFormHelperMock implements StubProvider {

    private LoginFormHelper.State state;
    private String refUrl;

    public LoginFormCtrlMock(LoginFormHelper.State state, String refUrl) {
        this.state = state;
        this.refUrl = refUrl;
    }

    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, 
            Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames, 
            List<Object> listOfArgs) {
        Object returnValue;
        
        if ('site_login'.equals(stubbedMethodName)) {
            returnValue = LoginFormHelper.State.LOGIN_OK == state
                ? new PageReference(refUrl)
                : null;
        }

        return returnValue;
    }
}
[/code]

[code]
@isTest
private class LoginFormCtrlTest {

    @isTest
    private static void testLoginOk() {
        Test.setCurrentPage(Page.LoginForm);

        LoginFormCtrl ctrl = new LoginFormCtrl();
        ctrl.helper = getHelperMock(LoginFormHelper.State.LOGIN_OK, '/');

        User communityUser = getCommunityUser();

        System.runAs(communityUser) {
            Test.startTest();

            ctrl.username = 'aaa';
            ctrl.password = 'aaa';

            System.assertEquals('/', ctrl.login().getUrl());
            System.assert(!ctrl.loginFailed);

            Test.stopTest();
        }
    }

    @isTest
    private static void testLoginKo() {
        Test.setCurrentPage(Page.LoginForm);

        LoginFormCtrl ctrl = new LoginFormCtrl();
        ctrl.helper = getHelperMock(LoginFormHelper.State.LOGIN_KO, null);

        User communityUser = getCommunityUser();

        System.runAs(communityUser) {
            Test.startTest();

            ctrl.username = 'aaa';
            ctrl.password = 'aaa';

            System.assertEquals(null, ctrl.login());
            System.assert(ctrl.loginFailed);

            Test.stopTest();
        }
    }

    private static LoginFormHelper getHelperMock(LoginFormHelper.State state, String refUrl) {
        LoginFormHelper helperMock = (LoginFormHelper) Test.createStub(
            LoginFormHelper.class,
            new LoginFormHelperMock(state, refUrl)
        );

        return helperMock;
    }

    private static User getCommunityUser() {
        User communityUser = [SELECT Id FROM User WHERE CommunityNickname = 'SiteName' LIMIT 1][0];

        return communityUser;
    }

}
[/code]

Писал код прямо в редакторе сообщения. Если будут ошибки, то пиши :) 

Developer
код

Эмммм.... Спасибо, конечно, но мне хотелось бы критику .

[quote="Developer"]код[/quote]
Эмммм.... Спасибо, конечно, но мне хотелось бы критику :p .

micha_s
Эмммм.... Спасибо, конечно, но мне хотелось бы критику .

1) Логику, которую мокают, лучше выносить в отдельный класс, чтобы проще было читать, обслуживать и тестировать.
2) Мок класс делать отдельно от тестов по той же причине.
3) В тестовом классе лучше делать отдельные методы под каждый тест кейс, а не один супер метод, который тестирует всё. В какой-то момент обслуживать такое становится просто невозможно.
4) LoginForm - непонятно какой это тип по коду(подозреваю, что это "LoginForm_Ctrl"). Да и имя переменной "stub" вносит только путанницу, т.к. скорее всего не относится к бизнес логике.

[quote="micha_s"]
Эмммм.... Спасибо, конечно, но мне хотелось бы критику :p .[/quote]

1) Логику, которую мокают, лучше выносить в отдельный класс, чтобы проще было читать, обслуживать и тестировать.
2) Мок класс делать отдельно от тестов по той же причине.
3) В тестовом классе лучше делать отдельные методы под каждый тест кейс, а не один супер метод, который тестирует всё. В какой-то момент обслуживать такое становится просто невозможно.
4) LoginForm - непонятно какой это тип по коду(подозреваю, что это "LoginForm_Ctrl"). Да и имя переменной "stub" вносит только путанницу, т.к. скорее всего не относится к бизнес логике.

Developer
выносить в отдельный класс
на текущий момент в проекте насчитывается более 550 классов. Если не так много кода - не считаю необходимым выносить.
Developer
отдельные методы под каждый тест кейс
Это - да, в моём примере упор был на понимание механизма и использование mock.
Developer
LoginForm
Developer
LoginForm_Ctrl
ууупссс. недоглядел.

В примере из хелпа dependency injection осуществляли посредством конструктора, и вот это меня путало больше всего.

[quote="Developer"]выносить в отдельный класс[/quote]на текущий момент в проекте насчитывается более 550 классов. Если не так много кода - не считаю необходимым выносить.
[quote="Developer"]отдельные методы под каждый тест кейс[/quote]Это - да, в моём примере упор был на понимание механизма и использование mock.
[quote="Developer"]LoginForm[/quote][quote="Developer"]LoginForm_Ctrl[/quote]ууупссс. недоглядел.

В примере из хелпа dependency injection осуществляли посредством конструктора, и вот это меня путало больше всего.