Написание Хелпера тест методов

Написание Хелпера тест методов

Всем здрасте!
Я работаю на достаточно большом проекте, и мне поставили задачу написать класс состоящий из функций для упрощения написания тест методов.

Задача для меня достаточно сложная и вместе с тем очень интересная. Думаю, что вопросов будет много, поэтому хотелось бы обсуждать их с вами по мере написания самого класса.

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

@isTest  

private class HelloTests {
static testMethod void Test1() {
Profile profile = [select id, Name from profile where name='System Administrator'];
User user = new User(alias = 'tt01', email='testtest01@test.com', emailencodingkey='UTF-8', lastname='testtest01', languagelocalekey='en_US', localesidkey='en_US', profileid = profile.Id, timezonesidkey='America/Los_Angeles', username='testtest01@test.com');
insert user;
System.RunAs(user){
// подготовка данных для тестирования
test.startTest();
// непосредственно тестирование (вызов методов, проверка возвращаемых данных)
test.stopTest();
}
}
}

Исходя из это и начал писать класс.

/** Function class

*
* createProfile() - create Profile "System Administrator"
* createTestUser() - create User "System Administrator"
*
*
**/

public with sharing class HelperTestMethods {

public static Profile createProfile(){
Profile profile = [select id from profile where name='System Administrator'];
return profile;
}


public static User createTestUser() {
Profile profile = HelperTestMethods.createProfile();

User user = new User(
alias = 'tt01',
email='testtest01@test.com',
emailencodingkey='UTF-8',
lastname='testtest01',
languagelocalekey='en_US',
localesidkey='en_US',
profileid = profile.Id,
timezonesidkey='America/Los_Angeles',
username='testtest01@test.com'
);

return user;
}

}

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

2. Названия функций давать такие, чтобы было понятно, что они делают. Имена могут получаться длинными, но как по мне такой код лучше читаем, хотелось бы узнать ваше мнение.

3. Этим кодом я уже упростил написание вот этого куска кода:

Profile profile = [select id, Name from profile where name='System Administrator'];  

User user = new User(alias = 'tt01', email='testtest01@test.com', emailencodingkey='UTF-8', lastname='testtest01', languagelocalekey='en_US', localesidkey='en_US', profileid = profile.Id, timezonesidkey='America/Los_Angeles', username='testtest01@test.com');
insert user;

вот такой конструкцией:
User testUser = HelperTestMethods.createTestUser();

insert testUser;

На этом этапе написания кода, критичных вопрос не возникло, но хотелось бы узнать, данная конструкция достаточно универсально или все же придется часто создавать, скажем профили с другими названиями или юзерам назначать какие-то другие переменные?

Ну и конечно же, если есть какие советы, буду очень рад.

Да, очень даже красиво получается.

Описание методов в начале очень правильно использовать, даже можно сказать обязательно (! главное не забывать вовремя исправлять описание, если что поменяется)

Теперь по поводу методов что ты придумал:
createProfile() и createTestUser() - я бы сделал по другому.

Во-первых, createProfile() - это не createProfile а больше getProfile получается, а во-вторых я бы не стал выносить один SOQL запрос в отдельный метод.

В общем оставляй createTestUser() и это будет твой первый helper метод (в нем и будет содержаться запрос профиля из базы)
Но вот я бы еще добавил параметр ProfileName, т.е. чтобы можно было указать пользователя с каким профилем ты хочешь создать.

чтобы получилось что-то типо этого:
User testUser = HelperTestMethods.createTestUser('System Administrator');

плюс
inser user;
return user;
должены быть в helper методе

Я тут воспользовался вашими совета Дмитрий и вот, что у меня получилось:

Во первых я сделал две функции, точнее одну функцию в двух возможных вариантах:

public static User createTestUser() {

Profile profile = [select id from profile where name='System Administrator'];

User user = new User(
alias = 'tt01',
email='testtest01@test.com',
emailencodingkey='UTF-8',
lastname='testtest01',
languagelocalekey='en_US',
localesidkey='en_US',
profileid = profile.Id,
timezonesidkey='America/Los_Angeles',
username='testtest01@test.com'
);

return user;
}

В этом случае функция вызывается User testUser = HelperTestMethods.createTestUser();

public static User createTestUser(String name) {

Profile profile = [select id from profile where name=:name];

User user = new User(
alias = 'tt01',
email='testtest01@test.com',
emailencodingkey='UTF-8',
lastname='testtest01',
languagelocalekey='en_US',
localesidkey='en_US',
profileid = profile.Id,
timezonesidkey='America/Los_Angeles',
username='testtest01@test.com'
);

return user;
}

В этом случае в функцию мы можем передать любой профиль, User testUser = HelperTestMethods.createTestUser('System Administrator');

Данная реализация мне показалось весьма хороша, так как чаще используется профиль "System Administrator", и в этом случае не нужно ничего передавать в функцию. А если нужен другой профиль просто передали его и функция тоже сработает.

И на этом этапе у меня возник такой вопрос. Можно ли как-нибудь сделать это одной функцией, а не вызывать функцию повторно. Например вот так, public static User createTestUser(String name = 'System Administrator'){}. Если не передавать аргумент, то по умолчанию переменной name будет назначено значение 'System Administrator'. Я искал документацию, но ничего подобного не нашел.

Дальше я сделал вот такую функцию:

public static User createAndInsertTestUser() {

Profile profile = [select id from profile where name='System Administrator'];

User user = new User(
alias = 'tt01',
email='testtest01@test.com',
emailencodingkey='UTF-8',
lastname='testtest01',
languagelocalekey='en_US',
localesidkey='en_US',
profileid = profile.Id,
timezonesidkey='America/Los_Angeles',
username='testtest01@test.com'
);
insert user;
return user;
}

public static User createAndInsertTestUser(String name) {
Profile profile = [select id from profile where name=:name];

User user = new User(
alias = 'tt01',
email='testtest01@test.com',
emailencodingkey='UTF-8',
lastname='testtest01',
languagelocalekey='en_US',
localesidkey='en_US',
profileid = profile.Id,
timezonesidkey='America/Los_Angeles',
username='testtest01@test.com'
);
insert user;
return user;
}

Здесь я исходил из следующих соображений, иногда нам требуется юзер с профилем "System Administrator" и нам нужно его заинсертить, тогда :

User testUser = HelperTestMethods.createAndInsertTestUser();

Если же нам требуется юзер с другим профилем, и нам не нужно какие особенные значения тогда:

User testUser = HelperTestMethods.createAndInsertTestUser('Standart User');

А вот если нам нужно добавить какие-то особенные значения, тогда так:

User testUser = HelperTestMethods.createTestUser('System Administrator');

testUser.timezonesidkey='Pacific/Samoa';
insert testUset;

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

Задумка понятна, но есть ряд замечаний:

Вот мой вариант кода, ниже я опишу почему я так сделал:

public static User createTestUser(String profileName) {

Profile profile = [select id from profile where name=:profileName];

User user = new User(
alias = 'tt01',
email='testtest01@test.com',
emailencodingkey='UTF-8',
lastname='testtest01',
languagelocalekey='en_US',
localesidkey='en_US',
profileid = profile.Id,
timezonesidkey='America/Los_Angeles',
username='testtest01@test.com'
);

instert user;
return user;
}

public static User createTestUser() {
return HelperTestMethods.createTestUser('System Administrator')
}

Всё! Это достаточный код для хелпера.

1. имена переменным старайся дать осмысленные (name -> profileName)
2. в методе createTestUser() не надо дублировать код createTestUser(String profileName) -> просто из метода без параметра вызываешь метод с параметром
3. не надо делать методы без insert (оба метода должны инсертить user и возвращать сохраненный объект) -> если кому-то будет не хватать каких либо полей, то можно после вывода метода сделать update user:

User testUser = HelperTestMethods.createTestUser('System Administrator');

testUser.timezonesidkey='Pacific/Samoa';
update testUset;

Так нужно сделать потому что в 90% случаев пользователь ожидает получить из твоего метода пользователя, чтобы с ним работать, а то что его нужно еще после этого insert в базу это как-то не совсем логичное действие, которое просто будут забывать выполнить и потом будут проблемы.

Возник интересный вопрос, не по хелперу, но по написанию тест методов.

есть переменная которая получает свое значение из некоторой функции какого-то другого контроллера.
И потом с этой переменной происходят разные манипуляции. Пример:

parsed = WebHttp.parseXML(query);

if(parsed.containsKey('result')){
и там много кода
}

Дмитрий где-то писал про такую конструкцию

if(Test.isRunningTest()) {

parsed = new Map<String,String>();
parsed.put('result', 'SUCCESS');
}else{
parsed = WebHttp.parseXML(query);
}

if(parsed.containsKey('result')){
и там много кода
}

И я этой конструкцией уже не раз пользовался

Но возникла такая задача, покрыть контроллер, в котором просто тьма if(){} блоков и всевозможных исключений,

Пример:

Obj = Controller.method(value);

if (jObj == NULL) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.ERROR, 'Null'));
return NULL;
}
if (Obj.has('error1')) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.ERROR, 'error1'));
return NULL;
}
if (Obj.has('error2')) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.ERROR, 'error2'));
return NULL;
}
if (jsObj.has('error3')) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.ERROR, 'error3'));
return NULL;
}

Мы передаем значение value, оно обрабатывается в какой-то функции которая, там что-то делает и возвращает какое-то значение. Проблема в данном случае это невозможность использовать больше одного значения if(Test.isRunningTest())

Так вот, а если использовать примерно следующее:

Obj = Controller.method(value);

nameFunction(Obj);

private nameFunction(nameObj Obj){
if (jObj == NULL) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.ERROR, 'Null'));
return NULL;
}
if (Obj.has('error1')) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.ERROR, 'error1'));
return NULL;
}
if (Obj.has('error2')) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.ERROR, 'error2'));
return NULL;
}
if (jsObj.has('error3')) {
ApexPages.addMessage(new ApexPages.Message(ApexPages.SEVERITY.ERROR, 'error3'));
return NULL;
}
}

И уже потом в тест методе запускать дополнительно данную функцию и передавать в нее объект с нужными значениями. Данный подход поможет не писать тестовые блоки в самом контроллере и покрыть много % кода. Скажите, что вы думаете по этому поводу.

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

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

Получается что в тестах ты вызываешь функцию nameFunction(Obj) N раз с необходимыми параметрами, чтобы протестировать твои N if блоков.

отличный метод.

Может я что то путаю но насколько я помню рекомендации Salesforce запрещает создавать новых User в тесте а рекомендует использовать уже существующих user и запускать тесты под ними если это необходимо.

Sergey Prichepo
Может я что то путаю но насколько я помню рекомендации Salesforce запрещает создавать новых User в тесте а рекомендует использовать уже существующих user и запускать тесты под ними если это необходимо.

Сергей, привет :)! Давно не заходил к нам!
Очень интересная информация, я про такое не слышал. Надо поискать подробности
Но вот если взять конкретный пример - разрабатываем приложение, которое устанавливается на сотни оргов. Получается что надо брать первого попавшегося пользователя и тестировать под ним? Как по мне это кот в мешке, потому что мы не знаем конкретно какие настройки у него выставлены (локали, языки, кастомные поля и т.д.)

У меня за время практики выработалась привычка вообще не использовать реальные данные (если это возможно) в тестах. Получается что-то типа автономной программы, со своим набором подготовленных данных. Так получается, что можно меньше опасаться проблем с отдельными клиентами.

Сергей, в моем случае, нужно создавать тестовых юзеров самому.

Теперь вот какой вопрос.
Есть необходимость вызывать некоторые методы много раз.

Пример:

User testUser1 = HelperTestMethods.createTestUser();

User testUser2 = HelperTestMethods.createTestUser();
User testUser3 = HelperTestMethods.createTestUser();

Код метода:

public static User createTestUser() {

Profile profile = [select id from profile where name='System Administrator'];

User user = new User(
alias = 'tt01',
email='testtest01@test.com',
emailencodingkey='UTF-8',
lastname='testtest01',
languagelocalekey='en_US',
localesidkey='en_US',
profileid = profile.Id,
timezonesidkey='America/Los_Angeles',
username='testtest01@test.com'
);
insert user;
return user;
}

как лучше сделать, чтобы каждый раз генерировалось новое имя для пользователя, я думаю лучше сделать отдельную функцию которая будет генерировать числа от 1 до скажем 1000. И тогда создавать имя примерно так.

String name = 'testUser' +  HelperTestMethods.методгенератор();

Тогда данный генератор можно будет использовать во многих функциях.
Что скажите мужики по этому поводу?

Проще в функцию HelperTestMethods.createTestUser()
добавить уникальный хеч к полям, которые должны быть уникальными.
Например добаить timestamp в username, email, nickname и что там еще должно быть уникальным

Вот как нибудь так:

String t = String.valueOf(datetime.now().getTime());

Получилось примерно так:

String timer = String.valueOf(DateTime.now().getTime());

User user = new User(
alias = 'tt' + timer,
email='test' + timer + '@test.com',
emailencodingkey='UTF-8',
lastname='Test' + timer,
languagelocalekey='en_US',
localesidkey='en_US',
profileid = profile.Id,
timezonesidkey='America/Los_Angeles',
username='TestTest' + timer + '@test.com'
);

Протестил и увидел такую ошибку, что поле alias max 8 символов, думаю сделать так, чтобы только последних 4 символа брались с getTime.

Да, все правильно!
Я так тоже делал - обрезал alias до нужного размера.

вариант решения, который предусматривает инициализацию филдов и множественный инсерт:

/// User.	

public static User createUser(Boolean isInsert, Map<String, Object> fieldValues){
Profile profile = TestDataFactory.getProfile();
String random = getRandomString(4);
String email = UserInfo.getUserEmail(); // or other
User user = new User(
alias = 'test' + random,
email = email,
emailencodingkey='UTF-8',
lastname = 'test user' + random,
languagelocalekey = Userinfo.getLanguage(),
localesidkey=Userinfo.getLocale(),
profileid = UserInfo.getProfileId(),
timezonesidkey=Userinfo.getTimeZone().getID(),
username = random + email
);

fillFieldValues(user, fieldValues);

if (isInsert){
insert user;
}

return user;
}

public static User createUser(Map<String, Object> fieldValues){
return createUser(true, fieldValues);
}

public static List<User> createUsers(Integer count){
List<User> users = new List<User>();

for(Integer i=0; i<count; i++){
users.add(createUser(false, null));
}

insert users;
return users;
}

// dynamic fill the Object's fields from the map.
@TestVisible
private static void fillFieldValues(sObject obj, Map<String, Object> fieldValues){
if (fieldValues!=null){
for (String fieldName : fieldValues.keySet()){
obj.put(fieldName, fieldValues.get(fieldName));
}
}
}

Красиво получилось dlisovsky , но как-то слишком наворочено (сперва написал другой ответ, более длинный, но потом перечитал и понял что слишком придираюсь)
В принципе все ты охватил все возможные ситуации: и просто создание пользователя и создание с сохранением, и создание пользователя с заданными данными в полях, и создание списка пользователей. Главное чтобы тот кто будет использовать эти методы не запутался в их обилии и не воспользовался по старинке User user = new User(...) :)

http://www.salesforce.com/us/developer/docs/apexcode/Content/apex_testing_load_data.htm

Cегодня узнал что есть еще и такой путь.

!!! мега полезная штука. Спасибо !!!

Даже вынесу описание сюда:

С помощью Test.loadData вы можете создавать данные в тест методах без использования кода. Просто добавьте данные в .csv файл, добавьте его в статик ресурсы и вызовите Test.loadData внутри тест метода с параметрами sObject type и имя статик этого ресурса. Например для создания записей Account из статик ресурса myResource код будет такой:
List<sObject> ls = Test.loadData(Account.sObjectType, 'myResource');

Очень интересно. Ссылка выше ведет на APEX developer Guide. В более простом APEX workbook об этом не упоминается.

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