Проблемы с UTs по мере роста Орга: возрастание лимитной нагрузки
Всем привет.
Недавно столкнулся с несколькими ситуациями, убедительно показывающими, как плохо сделанный код не раз аукается позже.
Есть ЮТ который тестирует довольно тяжелый функционал, и в процессе выполнения вычерпывает большую часть своих SOQL лимитов. Но все нормально, обычное дело.
И вот в один в день, кто-то тянет в Прод новый тригер, и те большие тесты ложаться по Лимитам.
В чем дело? В процессе подготовки тест даты втыкаются новые Контакты, которые провоцирует тот новый тригер, который добавляет SOQL операции и валит лимиты.
не буду говорить очевидные вещи как то, что нужно в тесте разделять подготовительную часть и тестовую, плюс тест в принципе можно расшить на два метода для увеличения лимитов.
Обратил внимание на следующее, в тесте несколько новых тест контактов инсертяться поочередно, а нужно бы втыкать одним Листом, чтоб не тревожить лишний раз тот новый тригер, который относительно балкафицирован.
а в тригере может быть прописано before insert, after insert без разделения логики по этому критерию, то есть получается он дважда делает ту же работу.
ну и что интересно, в тест дате связь между Аккаунтов и Контактом прописыватся вот так:
Contact c = new Contact(LastName='test', Email='test@gmail.com', Account=accObject);
а тригер ловит записи вот так: if (newContact.AccountID == null)
а что будет в поле Contact.AccountID перед инсертом, если задавать связь между записям как указано выше?
в общем, в большом Орге, по-ходу добавления нового функционала на стандартные объекты, Юнит тесты могут набирать лимитную нагрузку, пока...
Это доказано опытным путем и не один раз. Сейчас на проекте вставка одной записи может легко потянуть за собой вызов 10 триггеров на разных объектах. А потом индусы жалуются а чего это все так медленно работает. Я нашему так и на писал что если ты считаешь, что 3 секунды для запуска 10 триггеров это много сделай апгрейт салесфорса
Это проблема архитектуры юнит тестов. Если делать правильно, то никогда не появится такой проблемы: Делать правильно значит: - подготовка данных до Test.startTest() - после startTest вызов одного тестируемого метода - не знаю как правильно это называется с технической точки зрения - ОДНО действие пользователя, слышал так же entry point. НЕ БОЛЬШЕ! Да, получится дохрена тест методов, но тогда голова по лимитам не будет болеть.
Ну конечно в реальной жизни никто не будет так делать, обычно проще напихать в тест метод вызов всех тестируемых методов подряд чтобы сохранить состояние класса и воспроизвести работу view state. Тогда просто надо хотя бы отслеживать такие "тяжелые" тесты и просто ждать пока они отвалятся, чтобы принимать какие-то действия (разделять на тесты поменьше).
Кстати, а что с лимитами для @testSetup? У меня есть тысты, где я создаю пачку объектов ДО Test.startTest() и птм еще ПОСЛЕ него, ибо не хватает :-) Руки не дошли потестить.
спасибо, я как то спрашивал про одну книгу по тестам для JAVA, насколько инфа из нее могла бы быть полезной для нас и наших СФ тестов, но не получил четкого ответа. получается что эти книги по тестам для JAVA или C# могут полезны и нам...
наконец увидел что делается с тестами в Проде. Если запустить все тесты, то некоторые методы в некоторых из них валяться. Но если такой тест запустить там же отдельно, то нормально отрабатывает. Как так?
Плюс, я как то нашел в старом тесте и написал выше вот об этом:
Contact c = new Contact(LastName='test', Email='test@gmail.com', Account=accObject);
так вот, никакой связи между Контактом и Эккаунто в таком случае вовсе не создается! Создается без-Эккаунтный Контакт...
У тебя отключено параллельное выполнение тестов? У тебя нет @seeAllData в тестах? (хотя пока не понимаю как это может повлиять на твою ситуацию). В остальных случаях тесты ну никак не могут влиять друг на друга.
Не знаю как подтвердить, но возможно. У меня были проблемы с тестами, но при отключении параллельного выполнения все проходило. Я только могу сказать "мистика"
На счет параллельного выполнения. У меня были проблемы с этим - я использовал Custom Settings. Вот тут и происходила ошибка. Пара тестов пытались писать свои данные в эту одну запись. Вот и валились. Ессесно, если параллелизм выключен, то с этим проблем нет, только со скорость выполнения тестов.
Да что вы как дети малые. Ваши тесты НЕ ДОЛЖНЫ выполняться в параллельном режиме. И это уже вы должны сделать причем без всяких чекбоксов в настройках. Или используйте новую фишку setuptest вроде называется.
И как ты себе это представляешь? Да, я помню твой способ - запихнуть вызов всех тест методов в один метод и словить лимиты! Круто! Как можно "отключить без чекоксов" параллельное выполнение тестов? Я что-то так и не понял.
Может я чего-то не понимаю, но вот типичный пример из жизни: пакет с тестами в нем 100 тестовых классов в которых по 10 тест методов в каждом тест методе создается пользователь под которым выполняются тесты. Salesforce при включенном параллельном выполнении тестов в идеальном случае может запустить параллельно 1000 методов (если я правильно понимаю параллельно выполняются именно тест методы). И все эти 1000 методов попытаются создать пользователя с один и тем же username. КАК я могу из кода или какой-то архитектурной фишкой заставить все 1000 тестовых методов работать последовательно, по очереди?
Я уже об этом писал и не один раз. Делется один класс в котором делается несколько тест методов, в которые ты собираешь вызовы всех остальных методов тестовых классов.
НО КАК??? Если вызвать 10 тест методов которые кушают по 10 SQL в одном то получим 100 SQL в пределах этого метода. Так? Limit? Или ты знаешь способ как расширить лимиты в пределах ОДНОГО тест метода, который будет вызывать все остальные методы то поделись секретом. КАК Я СМОГУ ЗАПИХНУТЬ вызов 1000 методов в один??????????????????????????
Оу! То, что создалось в одном методе никак не появляется в другом. Откуда этот прикол, что если что-то создалось в одном методе, то оно будет видно в другом? Сделайте два теста в одном классе и параллельное включите. В одном создайте пользователя с именем ффф1, а в другом ффф2. И пущай вычитают двух пользователей хотя бы в одном методе. Будет по одному в каждом.
У меня тесты в одном классе НЕ выполняются последовательно (при включенном параллелизме). В каждом тесте, у мены вызывается один и тот же метод, который создает тестовый набор данных. Каждый раз, один и тот же, но тесты немного разные для разных случаев. 20 тестов и в каждом я генерирую данные.
Так вот, нужно ли во всех тестах указывать разное уникальное имя пользователя?
у меня могут быть и одинаковые имена в разных тестах, может они фейлят при паралелльном пуске?
У меня один метод - createTestData(Integer numberOfItems). Я его вызываю во всех тестах. Там прописанны имена объектов. Да, в неготорых местах я использую рандом. Уникальность на имена есть на объектах и ничего не валится при параллельном выполнении тестов.
Единственная проблема - Custom Settings - тут мне пришлось прибегнуть к ОЧЕНЬ плохой практике - делаю upsert объекта в while цикле, пока не создаст или обновит запись :-)
Более того из этого лога видно что запуск тестов происходит после сортировки по имени. Физически в классе первый метод у меня TestTriggers.
Просто вдруг вспомнил, если у вас есть страница и 2 контроллера, и вы передаете параметр на страницу, то вы должны его поймать тем котроллером, у которого имя окажется первым при сортировке, иначе параметр пропадет. Соорри, что не в тему, но вдруг кому-то будет полезно.
Что-то по твоим локам не совсем понятно что один тест метод запускается именно после окончания второго а не во время его работы. Не в смысле ты не прав, а в смысле не понятно из твоего примера. Более того вполне возможно что как раз они запустились параллельно и один тест стартанул быстрее чем другие поэтому и получилось то что имена пошли не в том порядке.
Более того вполне возможно что как раз они запустились параллельно и один тест стартанул быстрее чем другие поэтому и получилось то что имена пошли не в том порядке.
Ладно с этим переборщил, но все равно тоже попробую посмотреть этот вопрос по поводу последовательности тестов. Тоже любопытно что там в логах.
Пока вынужден согласиться с wilder - запускаются они последовательно (что можно наблюдать по времени вначале строк) и в отсортированном по имени порядке. Будем надеяться что это их нормальное повседневное поведение, которое не изменится при каких-то не зависящих от нас факторов в последующем.
PS. теперь осталось воспроизвести параллельное выполнение двух тест классов для полноты эксперимента.
PPS. Кстати заметил что в MavensMate в списке методы меняют свою последовательность. Сначала обрадовался что уличил wilder но потом по логам понял что последовательность с логами не связана (странно а с чем связана?)
Разрешите мне вставиться в вашу беседу со своими нехитрыми проблемами.
- тест ранится нормально в свежайшей фулл-копии прода и фейлится в проде на "mixed DML" (в обоих случаях был одиночный пуск внутри Орга).
- другой тест ранится нормально в орге, если запустить его с Эклипса, и фейлится на "mixed DML" при одиночном пуске внутри этого же Орга.
- также не понимаю механизма, почему при последовательном пуске пара тестов фейлится в Проде, но при попытке деплоя нового чендж сета никаких ошибок не выходит?
Разрешите мне вставиться в вашу беседу со своими нехитрыми проблемами.
- тест ранится нормально в свежайшей фулл-копии прода и фейлится в проде на "mixed DML" (в обоих случаях был одиночный пуск внутри Орга).
- другой тест ранится нормально в орге, если запустить его с Эклипса, и фейлится на "mixed DML" при одиночном пуске внутри этого же Орга.
- также не понимаю механизма, почему при последовательном пуске пара тестов фейлится в Проде, но при попытке деплоя нового чендж сета никаких ошибок не выходит?
На форумах очень полезно бывать. Я Очень много интересного тут узнал. Как показала практика люди в одной компании обычно развиваются в одном направлении и многие аспекты просто не захватывают. А тут народ из разных контор, с разным опытом. Это реально круто. Не понимаю своих бывших коллег, которые говорят что тут делать нечего, потому что тут одни школьники сидят. Ну пусть так дальше думают.
Кстати, я тут в соседней теме про сертификаты увидел высказывание, что до использования System.assertEquals() доходят не многие. А как тогда тестировать свой код? Как без этих проверок?
Я-то сталкивался, да очень редко. Я, обычно, сам работаю. Ну как, СФ программер я один. И тесты пишу я. И тесты я пишу так, чтоб проверить функционал без кликания мышкой в браузере или в приложениях. Я ооочень ленивый. Я пару раз видел инициализацию класса (контроллера в том случае) и вызовы метода с передачей параметров типа new sObject__c(field__c = 'qwerty'). Но это пару печальных случаев, кот перешли мне по наследству. А так... Надо искать какие-то шабашки, где больше одного СФ программера, чтоб было весело. А то я в своем мирке пидалю, а люди вон как делают :-)
Как то уж слишком эта проверка сурово выглядит. В salesforce и так количество SOQL запросов ограничено (весь остальной dev мир над нами смеется, хотя мы то понимаем, что это круто), а ты еще на в 10 раз хочешь это дело сократить. Это должно быть в виде Warning но никак не на уровне тестов.
Как то уж слишком эта проверка сурово выглядит. В salesforce и так количество SOQL запросов ограничено (весь остальной dev мир над нами смеется, хотя мы то понимаем, что это круто), а ты еще на в 10 раз хочешь это дело сократить. Это должно быть в виде Warning но никак не на уровне тестов.
Я тут недавно тупанул с функционалом и на проде выстрелило. По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста. У меня такого объема нет в песочнице, вот у меня и не выстрелило. А у меня с тестовым набором, который я могу сгенерировать до test.starttest() хватает максимум на два SOQL. А с тем кодом было около двадцати. Так что я себя обе запрашиваю.
По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста.
Вот это кстати большая проблема архитектуры кода. Количество SOQL запросов должно быть постоянным и не зависеть от количества данных. Значит где-то допущена грубейшая ошибка и SOQL попал в for.
По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста.
Вот это кстати большая проблема архитектуры кода. Количество SOQL запросов должно быть постоянным и не зависеть от количества данных. Значит где-то допущена грубейшая ошибка и SOQL попал в for.
Тупить надо меньше = )) Кстати да, в фор. Но подвох в том, что в форе создавался новый ДТО. И я в этот ДТО засунул запросы. Когда мне написали, что на проде вывалились ошибки, меня чуть сердце не остановилось. Это уловка для таких тупняков, которые на меня могут находить. Это было ужасно.
Решение есть, но оно работает только в одном случае и распространяется только на код за который вы несёте ответственость и/или к которому имеете доступ. По хорошему каждый unit test не должен зависеть от других unit test'ов и триггер соответственно от другого триггера, поэтому надо предусмотреть в триггере механизм позволяющий отключить его исполнение, банально через статическую переменную, например так (код показан в целях примера и не идеален):
//Class code public with sharing class AccountTrigger { public static Boolean HALT_EXECUTION = (!Test.isRunningTest() ? false : true); public void dispatch() { if (HALT_EXECUTION) { return; } //your code goes here } }
//Trigger code trigger onAccount on Account (before insert, ...) { new AccountTrigger().dispatch(); }
//Test class @istest public class AccountTriggerTest { @istest private static void accountTest() { AccountTrigger.HALT_EXECUTION = false; //test code here } }
Таким образом ваши триггера не подложат свинью другим, а за то что кто-то другой не приведёт к такой ситуации вы отвечать не можете.
А разве отключение триггеров в тестах не портит весь смысл тестирования? Если триггер конфликтуют между собой, то не лучше ли выявить это на этапе тестирования, а не в процессе использования? Если ваш триггер может подложить свинью другим это ли не повод узнать это в тестах?
А разве отключение триггеров в тестах не портит весь смысл тестирования? Если триггер конфликтуют между собой, то не лучше ли выявить это на этапе тестирования, а не в процессе использования? Если ваш триггер может подложить свинью другим это ли не повод узнать это в тестах?
Проблема - "возрастание лимитной нагрузки" - предложен инструмент для её решения. Как пользоваться этим инструментом и пользоваться ли им вообще - это уже другой вопрос.
И если уж на то пошло - unit test как раз и должен тестировать отдельный кусочек, а как всё работает вместе, это уже кажись интеграционное тестирование.
Кстати, да; я юнит тесты частенько не разделяю с интеграционными. У меня, по сути, и нет юнит тестов - одни интеграционные. Надо исправляться. Полезно читать форум :-)
Кстати, да; я юнит тесты частенько не разделяю с интеграционными. У меня, по сути, и нет юнит тестов - одни интеграционные.
Согласен, у меня наверное тоже самое. Ну это исторически сложилось на Salesforce. Теорию по тестированию не изучаем, а покрытие 75% сделать надо. На других платформах наверное когда дело доходит до тестов явно тема начинается с изучения теории.
Таким образом ваши триггера не подложат свинью другим, а за то что кто-то другой не приведёт к такой ситуации вы отвечать не можете.
Триггер не должен знать ничего о тестах, исходя из Single responsibility principle
Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.
Во-вторых - не вижу чем это противоречит single responsibility principle? Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.
В итоге получаем в триггере 100500 if только для тестов.
Во-вторых - не вижу чем это противоречит single responsibility principle?
Написал же, триггер знает о том, что существуют тесты.
Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
Я же просто высказал свое мнение. Тут большинство людей пишут все в одном классе со 100500 конструкциями if.
Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.
В итоге получаем в триггере 100500 if только для тестов.
If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).
Во-вторых - не вижу чем это противоречит single responsibility principle?
Написал же, триггер знает о том, что существуют тесты.
Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.
Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
Я же просто высказал свое мнение. Тут большинство людей пишут все в одном классе со 100500 конструкциями if.
If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).
Ну смотрите, вы говорите, что можете контролировать выполнение определенной логики в триггере. А разве их не может быть несколько? Просто разделение триггера на "мой" и "чужой" код выглядит не очень красиво.
Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.
Давайте обсудим, не правы как раз вы, суть принципов SOLID как раз в том, чтобы сделать архитектуру гибкой, для этого нужно использовать абстракции. Класс не должен знать о конкретной реализации других классов. Реализации в него нужно инжектить в виде абстракций. Tests.isTestRunning() - я считаю костылем. Использование этой конструкции подразумевает то, что у вас есть проблемы с покрытием класса тестами, а это явная ошибка архитектуры.
If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).
Ну смотрите, вы говорите, что можете контролировать выполнение определенной логики в триггере. А разве их не может быть несколько? Просто разделение триггера на "мой" и "чужой" код выглядит не очень красиво.
Не знаю упоминалось ли здесь на форуме, уверен что да, что почти каждый кто работет с форсом, рано или поздно, приходит к шаблону некоего диспатчера для триггера, который в зависимости от события передаёт управление в те или иные хелперы, сервисы и т.п. Предложенный подход как раз и предлагает выключать этот диспатчер и метод даже назван dispatch чтобы натолкнуть на эту мысль. Сами же хелперы, сервисы и прочее, тестируются в своих собственных unit test'ах. А вот дальше уже и идёт разделение ответсвтенности - я отвечаю за "мою" часть бизнес процесса, а другой разработчик за "свою".
Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.
Давайте обсудим, не правы как раз вы, суть принципов SOLID как раз в том, чтобы сделать архитектуру гибкой, для этого нужно использовать абстракции. Класс не должен знать о конкретной реализации других классов. Реализации в него нужно инжектить в виде абстракций. Tests.isTestRunning() - я считаю костылем. Использование этой конструкции подразумевает то, что у вас есть проблемы с покрытием класса тестами, а это явная ошибка архитектуры.
Я не против, это же был пример, никто не мешает вам завести абсракцию - например фабрику триггеров, которая вам в зависимости от контекста будет подсовывать соответствующий триггер/диспатчер, а для контекста test'а подсовывать например null object'овый класс-болванку, который ничего реально не делает. Единственное что есть в том числе и замечательный принцип KISS - зачем усложнять архитектуру без надобности, а чтобы появилась надобность надо чтобы был хоть один случай или ситуация, причём реальная, а не теоретическая, в которой предложенный подход не работает или увеличивает сложность.
KISS - зачем усложнять архитектуру без надобности, а чтобы появилась надобность надо чтобы был хоть один случай или ситуация, причём реальная, а не теоретическая, в которой предложенный подход не работает или увеличивает сложность.
Что в данном случае понимается под усложнением? Выполнение триггера может контролироваться в том числе и кастомными настройками, запущенным батчем и т.д. Вполне реальный пример? Я просто предлагаю инжектить это управление, как зависимость от текущего контекста.
Не знаю упоминалось ли здесь на форуме, уверен что да, что почти каждый кто работет с форсом, рано или поздно, приходит к шаблону некоего диспатчера для триггера, который в зависимости от события передаёт управление в те или иные хелперы, сервисы и т.п. Предложенный подход как раз и предлагает выключать этот диспатчер и метод даже назван dispatch чтобы натолкнуть на эту мысль. Сами же хелперы, сервисы и прочее, тестируются в своих собственных unit test'ах. А вот дальше уже и идёт разделение ответсвтенности - я отвечаю за "мою" часть бизнес процесса, а другой разработчик за "свою".
Вполне с этим согласен и разделение ответственности применимо не только к триггерам.
Очень интересная дискуссия. Говорю же, много интересных людей прибавилось последнее время.
Итак: не всегда наши ЮТ должны быть интеграционными, в случае когда доходит до проблем с лимитами, можно и отключить часть "дополнительной" логики. Правда в таком случае возникает вопрос, а кто будет писать интеграционный тест? Но с другой стороны если писать интеграционный тест, то в нем уже не гонешься за покрытием твоего кода, то есть в нем меньше логики можно вызвать, а значит проблем с лимитами может и не быть, ведь нужно просто "прозвонить" всю цепь событий.
А для отключения тригера в его сервис-классе(-сах) можно предусмотреть стат переменную. Но чтобы это действительно предварилось в жизнь, нужно делать требования ко всем тригерам иметь такую переменную...
Гоняться хуже, чем "прозванивать" Тем более при прозвонке большая часть и так покрывается. Ну и да, часто вижу их отсутствие, но не надо забывать про assert'ы. И да, привет, Илья)
//Trigger code trigger onAccount on Account (before insert, ...) { new AccountTrigger().dispatch(); }
постойте-ка, получается что все что есть в таком тригере - это одна строка инициализации класса и вызов метода (или вызов стат метода). понятно, что вся логика из тригера выводится в сервис слой, но здесь в тригере даже нет "разводки" логики по наВставку, наАпдейт, До, После. Т.е. абсолютно все делается в классе. А все контекстные переменные тригера подаются в аргументы конструктора класса или в метод. Или их и подавать в класс не нужно так как они будут доступны внутри метода (например как Trigger.new)???
И что нам дает такой "пустой" тригер с полностью выведенной логикой?
Во-первых, такой тригер покрыть тестом - как два пальца об асфальт: любая вставка записи покроет эту единственную строку.
Во-вторых, если мы все-таки передаем все контекстные переменные тригера в аргументы нашего класса, то получается, что для тестирования моего класса мне вовсе не обязательно, что-то куда-то ДМЛить: достаточно просто создать несколько объектов и подать их в конструктор или метод.
А что это нам дает? Ну например, полная модульность: если нужно добавить на объект новую логику, достаточно в тригере просто добавить еще одну строку с вывозом нового класса\метода.
Ты все правильно думаешь. Так же это позволяет нам собирать статистику о вложенности триггеров разрултвать ситуации с зацикливаниями и много что еще другого.
так все-таки, вы в свой "тригерный" класс\метод передаете контекстный переменные тригера как аргументы?
Выкладываю свой абстрактный класс, который использую для таких диспатчеров. Заодно оговорюсь - писался он давно, тесты содержит в себе самом (тогда так можно было и что бы там кто ни говорил - мне это нравилось, потому как не плодилось стопятьсот файлов и был дополнительный стимул делать классы компактными, чтобы даже с тестами они занимали разумное кол-во строк), global стоит т.к. класс входил в часть managed package, так что смело можно на public заменить. Со времени своего написания этой архитектуры хватало на 100% случаев с которыми приходилось с тех пор столкнуться.
/* Copyright (c) 2012 Illia Leshchuk aka Ilya Leshchuk All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. 4. Redistributions of source code are not permitted without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
global abstract class ATrigger { private static Boolean ALLOW_EXECUTION = false; global void execute() {
global virtual void hookBeforeInsert(List<SObject> records) {/** override if necessary */} global virtual void hookBeforeInsert(SObject record) {/** override if necessary */}
protected void onBeforeInsert(List<SObject> records) { hookBeforeInsert(records); for (SObject record : records) { hookBeforeInsert(record); } }
global virtual void hookBeforeUpdate(List<SObject> records, List<SObject> oldRecords) {/** override if necessary */} global virtual void hookBeforeUpdate(SObject record, SObject oldRecord) {/** override if necessary */}
protected void onBeforeUpdate(List<SObject> records, List<SObject> oldRecords) { hookBeforeUpdate(records, oldRecords); for (Integer i = 0; i < records.size(); i++) { hookBeforeUpdate(records.get(i), oldRecords.get(i)); } }
global virtual void hookBeforeDelete(List<SObject> records) {/** override if necessary */} global virtual void hookBeforeDelete(SObject record) {/** override if necessary */}
protected virtual void onBeforeDelete(List<SObject> records) { hookBeforeDelete(records); for (SObject record : records) { hookBeforeDelete(record); } }
global virtual void hookAfterInsert(List<SObject> records) {/** override if necessary */} global virtual void hookAfterInsert(SObject record) {/** override if necessary */}
protected void onAfterInsert(List<SObject> records) { hookAfterInsert(records); for (SObject record : records) { hookAfterInsert(record); } }
global virtual void hookAfterUpdate(List<SObject> records, List<SObject> oldRecords) {/** override if necessary */} global virtual void hookAfterUpdate(SObject record, SObject oldRecord) {/** override if necessary */}
protected void onAfterUpdate(List<SObject> records, List<SObject> oldRecords) { hookAfterUpdate(records, oldRecords); for (Integer i = 0; i < records.size(); i++) { hookAfterUpdate(records.get(i), oldRecords.get(i)); } }
global virtual void hookAfterDelete(List<SObject> records) {/** override if necessary */} global virtual void hookAfterDelete(SObject record) {/** override if necessary */}
protected void onAfterDelete(List<SObject> records) { hookAfterDelete(records); for (SObject record : records) { hookAfterDelete(record); } }
global virtual void hookAfterUndelete(List<SObject> records) {/** override if necessary */} global virtual void hookAfterUndelete(SObject record) {/** override if necessary */}
protected void onAfterUndelete(List<SObject> records) { hookAfterUndelete(records); for (SObject record : records) { hookAfterUndelete(record); } }
/********************UNIT TESTS REGION********************/ private class ConcreteTrigger extends ATrigger {/* exists only for testing sake */} private static @istest void testAll() { ConcreteTrigger testee = new ConcreteTrigger(); Schema.User record = new Schema.User(); List<Schema.User> records = new List<Schema.User>{record};
Для использования достаточно пронаследоваться от ATrigger и переопределить соответствующие hook-методы, в самом триггере достаточно инстанцировать конретный класс и дёрнуть execute метод - дальше класс вызовет соответствующие hook-методы.
Очень значимая проверка, не в обиду будет сказано.
А вот, кстати, проверка эта не такая дурацкая, как может показаться на первый взгляд. Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе. Так вот - не знаю как сейчас, а по тем временам тест, который не содержал ассертов считался неликвидным и отбраковывался чуть ли не на этапе автоматической проверки классов. А по-сути - покрывать в этом классе особо нечего, а этот ассерт обеспечивал выполнение того самого требования.
Очень значимая проверка, не в обиду будет сказано.
А вот, кстати, проверка эта не такая дурацкая, как может показаться на первый взгляд. Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе. Так вот - не знаю как сейчас, а по тем временам тест, который не содержал ассертов считался неликвидным и отбраковывался чуть ли не на этапе автоматической проверки классов. А по-сути - покрывать в этом классе особо нечего, а этот ассерт обеспечивал выполнение того самого требования.
Ну про проверку я в курсе, не зря ж написал, что не в обиду, просто логической ответственности он не несет.
Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе
Вот будет смешно, если они научатся оптимизировать и такие ассерты вообще удалятся при деплое (или не будут восприниматься). Ну и главный вопрос, что этот тест делает кроме покрытия?
protected void onBeforeInsert(List<SObject> records) { hookBeforeInsert(records); for (SObject record : records) { hookBeforeInsert(record); } }
hookBeforeInsert(records); - это подготовительный период перед перебором записей и операций с ними. На этой стадии мы должны одним махом выкверить всю инфу требуюмую в цикле ниже.
Но в этом методе hookBeforeInsert(records) мы можем объявить только локальные переменные, которые уже не будет доступны вот здесь: hookBeforeInsert(record)...
Но в этом методе hookBeforeInsert(records) мы можем объявить только локальные переменные, которые уже не будет доступны вот здесь: hookBeforeInsert(record)...
С чего бы это?
public class ObjectATriggerRouter implements ATrigger {
private ObjectB[] objectsB; ... public void override hookBeforeInsert(SObject[] records) { // заполняем objectsB или еще что-то ... }
protected void onBeforeInsert(List<SObject> records) { hookBeforeInsert(records); for (SObject record : records) { hookBeforeInsert(record); } }
hookBeforeInsert(records); - это подготовительный период перед перебором записей и операций с ними. На этой стадии мы должны одним махом выкверить всю инфу требуюмую в цикле ниже.
Но в этом методе hookBeforeInsert(records) мы можем объявить только локальные переменные, которые уже не будет доступны вот здесь: hookBeforeInsert(record)...
Не совсем, можете запомнить эти данные в переменной класса, который вы создали для переопределения ATrigger, тогда они будут доступны и в hookBeforeInsert(record) методе, например:
public with sharing class ConcreteTrigger extends ATrigger { private Object someVariable; public override void hookBeforeInsert(List<SObject> records) { someVariable = System.now(); }
остается только решить и согласовать с коллегами по цеху когда и какую стратегию выбрать: (1) делать простой тригер "все включено". (2) или тригер-разводчик с логикой выведенной в сервис методы. (3) или пустой тригер, где все выведено в такой класс как выше.
остается только решить и согласовать с коллегами по цеху когда и какую стратегию выбрать: (1) делать простой тригер "все включено". (2) или тригер-разводчик с логикой выведенной в сервис методы. (3) или пустой тригер, где все выведено в такой класс как выше.
Лично мой совет: Вариант 1 - не вариант: слишком тяжело контролировать порядок выполнения множества триггеров, если их много и слишком большой и сложный распухший триггер, если он будет включать в себя всех и вся; неудобство и сложность тестирования непосредственно триггеров.
Вариант 2, по-большому счёту, отличается от варианта 3 тем, что в варианте 3 получается 100% покрытие триггера тестами без каких-либо особых усилий, плюс гораздо легче добавить возможность вкл/выкл определенной логики, например через custom settings - в случае с триггером это сразу приведёт к падению покрытия тестами либо триггера, либо кода.
страшно подумать, как этого зверя представить другим разрабам. надо самому сначала все освоить и почувствовать.
Кстати есть особые случаи, это объекты общего пользования, логика на которых может дополняться с каждым проектом присоединившимся в Прод: как Контакты и Экаунты. Какой вариант будет оптимальным для них?
страшно подумать, как этого зверя представить другим разрабам. надо самому сначала все освоить и почувствовать.
Кстати есть особые случаи, это объекты общего пользования, логика на которых может дополняться с каждым проектом присоединившимся в Прод: как Контакты и Экаунты. Какой вариант будет оптимальным для них?
Повторюсь, вы в данном случае можете отвечать только за свой код.
Единственное что действительно надо запомнить - это порядок выполнения методов: hookInitialize -> hook on event (bulk) -> hook on event (single) -> hookFinalize, и hookException(Exception) если что-то пошло не так. А дальше имена методов, которые надо переопределить говорят сами за себя - если логика должна встраиваться (hook) перед (before) вставкой (insert) - hookBeforeInsert(List<SObject>), hookBeforeInsert(SObject) соответсввенно. При этом не обязательно переопределять всё это, достаточно в каком-то конкретном месте. Допустим вам надо запустить сложную логику если Opportunity.StageName поменялся с "In Progress" на "Closed Won", можно переопределить один метод hookBeforeUpdate:
public with sharing class OpportunityDispatcher extends ATrigger { public override void hookBeforeUpdate(SObject record, SObject prior) { dispatchComplex((Opportunity) record, (Opportunity) prior); }
при этом hookBeforeUpdate(SObject, SObject) запустится для каждого рекорда из контекста триггера. Если неразумно запускать этот метод отдельно для каждой записи - переопределить соответствующий bulk-метод, в данном случае hookBeforeUpdate(List<SObject>, List<SObject>).
взялся балкифицировать несколько тригеров. все они используют один и тот же метод у сервис класса, так что восходящее к sObject наследование использовалось на полную.
ок, балкифицировал тот метод: вся потенциально нужная инфа квериться до "главного" цикла с перебором "пришедших" записей (по возвожности используются такие "АПИ" методы как Account.....get('Sample').getRecordTypeId();) и подается в цикл по возможности в виде Мапов, и ДМЛ операции (если они есть в тригере) выводятся после главного цикла.
готово. но логика то относительно сложная: в зависимости от содержания трех полей на записи логика разная. как проверить что мой мэйджик метод работает как надо? подключить его к живому сендбоксу и покликать по интерфейсу?
тут то я начал понимать, что значит словочетание "unit testing"
написал код, который тестит работу моего метода по 6 потенциальным кейсам, и проверяет на выходе все измененные поля. 30 систем асертов в нем - но я еще не подключил мой метод никуда - но совершенно точно знаю, что то что он должен делать - он делает.
затем осталось только подключить метод в тригер и написать тест для тригера в котором я испытываю тригер на устойчивось к батч-операциям. Здесь уже логика не проверятся. Но покрывается тригер и определяется устойчивость к батчам на уровне самого тригера, не на уровне метода, думаю так лучше, так как это более приближенно к реальности.
Ну что ж, осталось только приодеть мои тригеры в тот костюмчик от financialforce о котором говорили выше. но это уже будет другая история
ATrigger, описанный выше, кажется более удобным для начала...
Лично я вообще не вижу смысла в классах типа ATrigger, у меня вся логика описана в сервисах, а в триггере просто вызываются нужные методы из сервиса. ATrigger нужен тем, кто не любить выносить логику в сервисы.
Лично я вообще не вижу смысла в классах типа ATrigger, у меня вся логика описана в сервисах, а в триггере просто вызываются нужные методы из сервиса. ATrigger нужен тем, кто не любить выносить логику в сервисы.
у меня тоже сейчас логика в балкафицированных методах сервис класса.
и в ATrigger я собирался их вызывать как сейчас вызываю в тригере.
так зачем ATrigger нужен? - для унификации конструкции тригера в т.ч. наличие отсечки - у меня есть надежда, что один класс наследник ATrigger я смогу использовать в тригерах на разных объектах, в которых я сейчас использую абсолютно идентичную логику, а у меня больше десяти таких объектов.
- для унификации конструкции тригера в т.ч. наличие отсечки
Не ижу в этом плюсов
- у меня есть надежда, что один класс наследник ATrigger я смогу использовать в тригерах на разных объектах, в которых я сейчас использую абсолютно идентичную логику, а у меня больше десяти таких объектов.
Если логика идентична, почему бы ее не собраться в одном методе и не вызывать в триггере
Если логика идентична, почему бы ее не собраться в одном методе и не вызывать в триггере
там логика идентична во всех четырех ветках: до\после, вставка\апдейт.
myConcreteATrigger.execute();
вот этот метод и мог бы быть тем самым методом.
плюс возможно я смогу ре-юзнуть myConcreteATrigger.execute() во всех тригерах где требуется эта логика.
а если тригер нужно дополнить новой, присущей только этому объекту логикой, то новая логика будет в новом mySecondConcreteATrigger который пойдет второй (или первой) строкой в требуемый тригер:
Тесты в большом Орге порой валяться по каким то неожиданным причинам.
Вот только выпал тест в котором на data-preparation стадии вставлятся Аккаунт, которому приписывается определенный РекТайп.
При очередном запуске что-то не понравлось в РекТайпе: "INVALID_CROSS_REFERENCE_KEY, Record Type ID: this ID value in't valid for the user."
надо спросить, под каким юзером\профайлом двигали тот ЧС и заранили тесты...
PS: что-то у меня возникло нехорошее предчувствие, что data-preparation фазу теста нужно выполнять под определенным Юзером (СисАдмином), иначе после начнутся чудеса, так как приписка RT для тестовой записи - это почти обязательная часть любого теста...
Тесты в большом Орге порой валяться по каким то неожиданным причинам.
Вот только выпал тест в котором на data-preparation стадии вставлятся Аккаунт, которому приписывается определенный РекТайп.
При очередном запуске что-то не понравлось в РекТайпе: "INVALID_CROSS_REFERENCE_KEY, Record Type ID: this ID value in't valid for the user."
надо спросить, под каким юзером\профайлом двигали тот ЧС и заранили тесты...
PS: что-то у меня возникло нехорошее предчувствие, что data-preparation фазу теста нужно выполнять под определенным Юзером (СисАдмином), иначе после начнутся чудеса, так как приписка RT для тестовой записи - это почти обязательная часть любого теста...
Тут ситуация в том что профилю пользователя, под которым выполняется тест, недоступен тот RT который в тесте пытаются проставить. Под System Administrator запускать тест не обязательно, просто если вы тестируете ситуацию с конкретным RT - создайте условия (например System.runAs) чтобы тестовому пользователю он был доступен, а если RT несущественный - не проставляйте его явно - подхватится тот что по-умолчанию.
Что значит по неожиданных причинам? Это как-то странно звучит в сфере IT. У всего есть своя причина. Если тесты запускает неизвестно кто, лучше себя обезопасить и создавать в тестах системного юзера под которыми потом делать RunAs() Я всегда так делаю, если у заказчика есть свободная лицензия для админа (этот способ кушает одну лицензию). Был один заказчик у которого все лицензии Salesforce использовались, этот способ у него не работал. Зато, кто бы не запустил тесты, можно быть уверенным что контекст не повлияет на их исполнение. Вопрос второй. Если и это не помогает и ошибка с RecordType возникает, то надо обратить внимание как этот рекорд тайп получается из базы. Самое прикольное что я никогда не видел чтобы у заказчика (делали до меня) это дело было реализовано правильно.
Вот как должен выглядеть запрос: SELECT Id FROM RecordType WHERE DeveloperName = 'SomeName' AND SobjectType = 'Contact' AND isActive = true Обязательно что-то да упустят, либо вместо DeveloperName просто Name, либо SobjectType не укажут, либо про isActive забудут и получают неактивные рекорд тайпы, которые при вставке в поле записи возвращают ошибку выше.
Кстати, хороший вопрос. А я ничего не забыл в запросе RecordType выше? Как проверить что у пользователя есть доступ к данному RecordType?
Тут ситуация в том что профилю пользователя, под которым выполняется тест, недоступен тот RT который в тесте пытаются проставить. Под System Administrator запускать тест не обязательно, просто если вы тестируете ситуацию с конкретным RT - создайте условия (например System.runAs) чтобы тестовому пользователю он был доступен, а если RT несущественный - не проставляйте его явно - подхватится тот что по-умолчанию.
я так и подумал
Если тесты запускает неизвестно кто, лучше себя обезопасить и создавать в тестах системного юзера под которыми потом делать RunAs() Я всегда так делаю, если у заказчика есть свободная лицензия для админа (этот способ кушает одну лицензию)
Что?! чтобы заранить под созданным (как тест дата) СисАдмин юзером нужно иметь свободную лицензию? Не такая уж редкость когда все лицензии в Проде выбраны.
А что если какого-нибудь СисАдмин выкверить? нет,для этого нужно открыть seeAllData, не хорошо.
наверное, проще попросить их более внимательно отнестить к профайлу юзера, который запускает тесты..
А что если какого-нибудь СисАдмин выкверить? нет,для этого нужно открыть seeAllData, не хорошо.
То что у вас seeAllData=false, вовсе не означает что база полностью пустая - куча системной информации там есть и вполне себе доступна. За полным списком надо в доку лезть, но я на 100% уверен, что это как минимум Users, Profiles и RecordTypes.
Если тесты запускает неизвестно кто, лучше себя обезопасить и создавать в тестах системного юзера под которыми потом делать RunAs() Я всегда так делаю, если у заказчика есть свободная лицензия для админа (этот способ кушает одну лицензию). Был один заказчик у которого все лицензии Salesforce использовались, этот способ у него не работал.
Попробуйте в тесте деактивировать пользователя с соответствующим типом лицензии и только после этого вызывать System.runAs c нужным вам пользователем.
Интересная идея! Попробую как подвернется случай. У того клиента, я обычно деактивировал пользователя вручную на момент запуска тестов. Но это естесвенно было с разрешения клиента и клиент сам говорил кого кем можно пожертвовать
System.DmlException: Insert failed. First exception on row 0; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, My_Super_Trigger: execution of AfterInsert
caused by: System.DmlException: Update failed. First exception on row 0 with id a2w2900000009bxCCA; first error: UNABLE_TO_LOCK_ROW, unable to obtain exclusive access to this record: []
Что, параллельно уже не получиться выполнять тесты? И тупо через раз валится эта ошибка. Один раз все тесты запускаю - проходят, второй - валится.
А на другом орге нет такой проблемы. И, кстати, на этом другом орге тесты запускаются ОООЧЕНЬ медленно. Могу минут пять ждать запуска тестов. Никто с такими приколами не сталкивался?
System.DmlException: Insert failed. First exception on row 0; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, My_Super_Trigger: execution of AfterInsert
caused by: System.DmlException: Update failed. First exception on row 0 with id a2w2900000009bxCCA; first error: UNABLE_TO_LOCK_ROW, unable to obtain exclusive access to this record: []
Что, параллельно уже не получиться выполнять тесты? И тупо через раз валится эта ошибка. Один раз все тесты запускаю - проходят, второй - валится.
А что это за объект на котором происходит LOCK? И часом там не используется seeAllData=true или версия API ниже 24 или сколько там надо чтобы seeAllData был true по-умолчанию? Просто на вскидку не понятно почему возникает lock, если по-идее каждый тест должен выполняться в независимом окружении при seeAllData=false.
Возможно в этом и причина. Я думаю что гораздо труднее будет сохранить seeAllData=true, чтобы использовать для тестов имеющиеся данные, если это так критично, и при этом разработать механизм или отрефакторить код так чтобы не возникало блокировки, чем просто переписать тесты с использованием seeAllData=false и созданием тестовых данных.
У меня уже несколько методов, кот видят данные. Смысла писать код, для того, что бы тесты все скопом работали, я не вижу. А прогонять некоторый функционал на большом количестве объектов надо. Придется только ждать тестов. Спасибо, я как-то на SeeAllData=true и не посмотрел.
Используется SeeAllData=true. Как раз в этом тесте, кот отмечен.
Ох и плохая это идея. Не знаю как вы, а я к этому отношусь крайне негативно. Тесты все-таки должны работать в сферическом вакууме и не зависеть не от пользователя, который запускает тесты, ни от реальных данных. Ладно, это еще можно допустить при разработке отдельного решения для заказчика, но если пакет, то однозначно нет.
Используется SeeAllData=true. Как раз в этом тесте, кот отмечен.
Ох и плохая это идея. Не знаю как вы, а я к этому отношусь крайне негативно. Тесты все-таки должны работать в сферическом вакууме и не зависеть не от пользователя, который запускает тесты, ни от реальных данных. Ладно, это еще можно допустить при разработке отдельного решения для заказчика, но если пакет, то однозначно нет.
Я этого тоже не люблю. Но после того, как пару раз вылезла проблемы на проде, я пишу дополнительно тесты с живыми данными. Я не могу сгенерировать достаточное количество данных в тесте. Так что приходится так.
Не, на проде другая проблема - Total number of records retrieved by SOQL queries. На дэв я создать не могу много из-за SOQL Limits, чтоб протестить Total number of records retrieved by SOQL queries проблему.
Не, на проде другая проблема - Total number of records retrieved by SOQL queries. На дэв я создать не могу много из-за SOQL Limits, чтоб протестить Total number of records retrieved by SOQL queries проблему.
Не, на проде другая проблема - Total number of records retrieved by SOQL queries. На дэв я создать не могу много из-за SOQL Limits, чтоб протестить Total number of records retrieved by SOQL queries проблему.
Тестирование, на сколько я понял, начинается с того что "Допустим у нас очень много данных!" Поступите другим способом - добавьте в ваши триггера возможность их отключения (например через Custom Settings) и в тесте, когда заполняете базу данными просто их отключите. Это позволит вам хотя бы не упереться в SOQL Queries лимит.
Не, на проде другая проблема - Total number of records retrieved by SOQL queries. На дэв я создать не могу много из-за SOQL Limits, чтоб протестить Total number of records retrieved by SOQL queries проблему.
Тестирование, на сколько я понял, начинается с того что "Допустим у нас очень много данных!" Поступите другим способом - добавьте в ваши триггера возможность их отключения (например через Custom Settings) и в тесте, когда заполняете базу данными просто их отключите. Это позволит вам хотя бы не упереться в SOQL Queries лимит.
Мне нельзя отключать. Триггер обновляет зависимые объекты. Вычитываются другие объекты, обрабатываются и снова вставка/обновление. Без этой обработки невозможно протестить функциональность. Единственное, что у меня есть, так это флаги firstRun. Я не пишу юнит тесты (хотя, видимо, пора начинать), я пишу функциональные тесты. Данные генерирую для каждого теста так, чтобы протестить все возможные случаи.
Я думаю ilya leshchuk имел в виду что перед тем как запускать тесты нужны тестовые данные, которые необходимо создать, а создать тебе мешают лимиты из-за навешенных на объектах триггерах. Так вот чтобы просто создать тестовые данные без проблем с лимитами можно всю функциональность нафиг отключить, потом включить и уже тесты гонять. Это чтобы не делать SeeAllData=true.
Мне нельзя отключать. Триггер обновляет зависимые объекты. Вычитываются другие объекты, обрабатываются и снова вставка/обновление. Без этой обработки невозможно протестить функциональность. Единственное, что у меня есть, так это флаги firstRun. Я не пишу юнит тесты (хотя, видимо, пора начинать), я пишу функциональные тесты. Данные генерирую для каждого теста так, чтобы протестить все возможные случаи.
Может я неправильно выразился, имел в виду: отключить триггера, сгенерить кучу данных, включить их обратно, протестировать. Правда при отключенных триггерах может быть проблема с той самой генерацией данных.
Почему бы не использовать класс для рекурсивного создания данных на основе каких-то принципов?
Что ты имеешь в виду под "рекурсивное создание"? Я создаю сначало List<obj1> list01. Птм создаю List<obj2> list02. Каждому obj2 в цикле назначается разный obj1. После инсерта list02 просчитываются поля на obj1 и obj2. Ну а когда дело доходит до obj5, то там пара списков вычитывается и пара списков обновляется.
Ну например, в зависимости от поля Статус на obj2 выставляется Статус на obj1. И надо вычитать все obj2 для каждого obj1, что правильно посчитать Статус. Если тригер выключить, то статус не посчитается и я не смогу протестить функционал.
сижу, пробую запилить свой первый тригер на абстрактном каркасе ATrigger. по-ходу всплывают разные вопросы:
- зачем ALLOW_EXECUTION изначально в false, и когда его лучше переводить в Тру?
получается что этот выключатель, будучи на родительском классе, сработает одинаково для всех "триггеров" (унаследованных от него) в рантайме?
- не понял как лучше обработать исключения. Вот это есть, но как использовать еще не додумался:
например, мой класс делает ДМЛ и отправляет письма, если исключение произошло с ДМЛ - то нужно сделать красивое сообщение на записи, а если с письмами - то пусть падает как есть.
получается что это нужно организовывать в самом тригере: ловить траем, определять что за исключение и т.д. выглядит как-то неправильно...
ATrigger не будет работать не будучи запущенным в контексте триггера, а эта переменная добавлена для возможности обойти этот запрет в UT разрешив запуск вне контекста триггера.
получается что это нужно организовывать в самом тригере: ловить траем, определять что за исключение и т.д. выглядит как-то неправильно...
Зачем? Переопределяете метод hookException например так:
public override void hookException(Exception e) { if (e instanceof ExceptionDML) { //do something } else if (e instanceof ExceptionB) { //do something else... } else { //fallback } }
Пока писал предыдущий пост, пришла в голову идея: иметь специальный класс exception'ов, из которых можно восстановится, соответствующий интерфейс, например
public interface IRecoverableException { Object recover(); }
...
public override void hookException(Exception e) { if (e instanceof IRecoverableException) { ((IRecoverableException) e).recover(); } //other code here }
у меня не много опыта работы в Проде, поэтому еще есть много "наивных" вопросов по тестированию в Проде.
вот они установили managed package в Прод. Как я понимаю, покрытие кода из этого пакета также складывается в общее покрытие Орга, как и наш собственный код?
Спрашиваю, так как один проект уставил в общий Прод managed package, в котором упало значительное кол-во тестов. Всем по-барабану, так как вроде приложение работает. Но тут выяснилось, что общее покрытие упало до 74% и не возможно задвинуть новые пакеты. Тут начались волнения, что мол делать, как дальше жить...
Как можно иметь в Проде приложение у которого попадало половина тестов? ведь это не только влияние на общее покрытие, а признак того, что часть функционала приложения просто не работает...
Это очень плохо если нет человека который это понимает, программиста который за это отвечает. Сам пару раз сталкивался с такими проектами - нанимали человека когда-то сделать кусок функционала - тот делал не очень качественно и умывал руки. Со временем тесты на проде начали валиться, но всем пофиг, потому что "вроде" работает.
Мне очень жаль такие проекты - они потихоньку умирают. Над каждым кодом должен кто-то стоять, за каждым кодом должен кто-то следить. Пусть и не постоянно, на основе "поддержки", но это должно быть.
Короче, по твоему вопросу, Den. Конечно надо разбираться почему упали тесты и чинить их. Тут ничего не поделаешь. И если заказчик не хочет платить за то что ты будешь чинить эти косяки, то пусть ищут тех кто виновен и трясти их.
Нет, покрытие кода из managed package не влияет на общее покрытие кода в организации.
Само покрытие managed package на среднее покрытие кода в организации не влияет, но вот package может влиять опосредованно, а именно завалив существующие тесты. Например какой-нибудь Country Complete managed package добавляет валидацию на поля "country", на соответствие их какому-нибудь ISO стандарту, да ещё и по-умолчанию навешивается на парочку стандартных объектов - Contact, Account и т.д. С большой долей вероятности эти стандартные объекты используются в существующих тестах и теперь эти тесты могут нечаянно перестать работать.
ну вот, уже легче, значит нужно подправить только собственные тесты. надо бы проверить, влияет ли на них тот пакетдж, но не думаю, что там могут быть какие то грубые косяки с общиими объектами.
Тоже не 100% вариант. Возможно твой функционал влияют на тесты внутри managed package. Например это у тебя есть какое нибудь required поле на стандартном объекте, которое не дает тестам из пакета вставить запись, потому что пакет не знает об этом поле.
Этот вопрос уже я поднимал - почему плохо задействовать стандартные объекты для managed package.
Что-то не совсем понятно. Кто будет придумывать и кто будет использовать. Если свои пакета, может быть. А если ты ставишь пакеты от какого нибудь КогаддуБираппаТимаппаНаира, как ты это дело собираешься под твою концепцию подстраивать?
Если бы ты не говорил что собираешься заняться своим делом, я бы подумал что в ближайшее время ты собираешься занять один из руководящих постов в департаменте разработки Salesforce.
когда в проде калькулируется процент среднего покрытия, то как идет расчет среднего процента:
суммируются проценты покрытия каждого класс\тригера и из этой суммы вычисляется средний процент или суммируется общее кол-во строк и затем строк покрытого кода, и в конце выводится средний процент по Оргу?
в контроллере создаются дочерние записи и вставляются. но у него есть метод "клонирующий" предыдущий кастомерский ввод, т.е. эти дочерние записи используются повторно, у них просто переписывается значение в Master-Detail поле.
и все работает ОК.
а в тесте не работает: это Master-Detail поле становится неРедактируемым после вставки записи. Долго думал, что за ерунда.
Оказывается, что это Master-Detail поле действительно становится неРедактируемым после вставки записи. Но в реальном контроллере вставка и перезапись происходит в разных обращениях к серверу, и поле перезаписыватся. А в тесте я пытался последовательно вызывать методы у одного и того же экземпляра Контроллера, и там такой номер не прошел. пришлось несколько раз создавать контроллер.
А можно с примером кода из контроллера и в идеале со скриншотом полей объектов. Просто, судя по написанному, такого быть не может - если поля не reparentable, то переподвязать чайлды к новому паренту не получится, максимум что происходит в контроллере: старые чайлды клонируются, им проставляются лукапы на новых парентов, новые чайлды вставляются в базу, а старые удаляются.
То что надо создавать новый экземпляр Контроллера говорит о том что вы не до конца разобрались с проблемой, это никоим образом не должно влиять на данный функционал.