Test.createStub

Test.createStub

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

Спасибо.

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

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 фреймворком, т.к. просто не было времени раньше на проектах(приходилось фичи пилить, а не тесты оптимизировать), но сейчас с радостью обсужу со всеми, раз уже есть время.

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

Спасибо.

Уже года 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;
}

}

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

}

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

Developer
код

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

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

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

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

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

Interesting information? Help us, post link to social media..