Проблемы с 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 перед инсертом, если задавать связь между записям как указано выше?
в общем, в большом Орге, по-ходу добавления нового функционала на стандартные объекты, Юнит тесты могут набирать лимитную нагрузку, пока...
Всем привет.
Недавно столкнулся с несколькими ситуациями, убедительно показывающими, как плохо сделанный код не раз аукается позже.
Есть ЮТ который тестирует довольно тяжелый функционал, и в процессе выполнения вычерпывает большую часть своих SOQL лимитов. Но все нормально, обычное дело.
И вот в один в день, кто-то тянет в Прод новый тригер, и те большие тесты ложаться по Лимитам.
В чем дело? В процессе подготовки тест даты втыкаются новые Контакты, которые провоцирует тот новый тригер, который добавляет SOQL операции и валит лимиты.
не буду говорить очевидные вещи как то, что нужно в тесте разделять подготовительную часть и тестовую, плюс тест в принципе можно расшить на два метода для увеличения лимитов.
Обратил внимание на следующее, в тесте несколько новых тест контактов инсертяться поочередно, а нужно бы втыкать одним Листом, чтоб не тревожить лишний раз тот новый тригер, который относительно балкафицирован.
а в тригере может быть прописано before insert, after insert без разделения логики по этому критерию, то есть получается он дважда делает ту же работу.
ну и что интересно, в тест дате связь между Аккаунтов и Контактом прописыватся вот так:
Contact c = new Contact(LastName='test', Email='test@gmail.com', [b]Account=accObject[/b]);
а тригер ловит записи вот так: if (newContact.AccountID == null)
а что будет в поле Contact.AccountID перед инсертом, если задавать связь между записям как указано выше?
в общем, в большом Орге, по-ходу добавления нового функционала на стандартные объекты, Юнит тесты могут набирать лимитную нагрузку, пока...
пока ты не сделаешь все как надо.
Это доказано опытным путем и не один раз. Сейчас на проекте вставка одной записи может легко потянуть за собой вызов 10 триггеров на разных объектах. А потом индусы жалуются а чего это все так медленно работает. Я нашему так и на писал что если ты считаешь, что 3 секунды для запуска 10 триггеров это много сделай апгрейт салесфорса
[quote="Den Brown"]Недавно столкнулся с несколькими ситуациями, убедительно показывающими, как плохо сделанный код не раз аукается позже.[/quote]
Это доказано опытным путем и не один раз. Сейчас на проекте вставка одной записи может легко потянуть за собой вызов 10 триггеров на разных объектах. А потом индусы жалуются а чего это все так медленно работает. Я нашему так и на писал что если ты считаешь, что 3 секунды для запуска 10 триггеров это много сделай апгрейт салесфорса :)
Это проблема архитектуры юнит тестов. Если делать правильно, то никогда не появится такой проблемы: Делать правильно значит: - подготовка данных до Test.startTest() - после startTest вызов одного тестируемого метода - не знаю как правильно это называется с технической точки зрения - ОДНО действие пользователя, слышал так же entry point. НЕ БОЛЬШЕ! Да, получится дохрена тест методов, но тогда голова по лимитам не будет болеть.
Ну конечно в реальной жизни никто не будет так делать, обычно проще напихать в тест метод вызов всех тестируемых методов подряд чтобы сохранить состояние класса и воспроизвести работу view state. Тогда просто надо хотя бы отслеживать такие "тяжелые" тесты и просто ждать пока они отвалятся, чтобы принимать какие-то действия (разделять на тесты поменьше).
[quote="Den Brown"]Есть ЮТ который тестирует довольно тяжелый функционал, и в процессе выполнения вычерпывает большую часть своих SOQL лимитов. Но все нормально, обычное дело.[/quote]
Это проблема архитектуры юнит тестов.
Если делать правильно, то никогда не появится такой проблемы:
Делать правильно значит:
- подготовка данных до Test.startTest()
- после startTest вызов одного тестируемого метода - не знаю как правильно это называется с технической точки зрения - ОДНО действие пользователя, слышал так же entry point. НЕ БОЛЬШЕ!
Да, получится дохрена тест методов, но тогда голова по лимитам не будет болеть.
Ну конечно в реальной жизни никто не будет так делать, обычно проще напихать в тест метод вызов всех тестируемых методов подряд чтобы сохранить состояние класса и воспроизвести работу view state. Тогда просто надо хотя бы отслеживать такие "тяжелые" тесты и просто ждать пока они отвалятся, чтобы принимать какие-то действия (разделять на тесты поменьше).
С введением @testSetup стало гораздо проще писать тесты
Еще можешь заставить разработчиков прочитать [url=http://artofunittesting.com/]Art Of Unit Testing[/url]
Кстати, а что с лимитами для @testSetup? У меня есть тысты, где я создаю пачку объектов ДО Test.startTest() и птм еще ПОСЛЕ него, ибо не хватает :-) Руки не дошли потестить.
[quote="Gres"]С введением @testSetup стало гораздо проще писать тесты[/quote]
Кстати, а что с лимитами для @testSetup?
У меня есть тысты, где я создаю пачку объектов ДО Test.startTest() и птм еще ПОСЛЕ него, ибо не хватает :-) Руки не дошли потестить.
спасибо, я как то спрашивал про одну книгу по тестам для JAVA, насколько инфа из нее могла бы быть полезной для нас и наших СФ тестов, но не получил четкого ответа. получается что эти книги по тестам для JAVA или C# могут полезны и нам...
наконец увидел что делается с тестами в Проде. Если запустить все тесты, то некоторые методы в некоторых из них валяться. Но если такой тест запустить там же отдельно, то нормально отрабатывает. Как так?
Плюс, я как то нашел в старом тесте и написал выше вот об этом:
Contact c = new Contact(LastName='test', Email='test@gmail.com', Account=accObject);
так вот, никакой связи между Контактом и Эккаунто в таком случае вовсе не создается! Создается без-Эккаунтный Контакт...
[quote="Gres"]Еще можешь заставить разработчиков прочитать [url=http://artofunittesting.com/]Art Of Unit Testing[/url][/quote]
спасибо, я как то спрашивал про одну книгу по тестам для JAVA, насколько инфа из нее могла бы быть полезной для нас и наших СФ тестов, но не получил четкого ответа. получается что эти книги по тестам для JAVA или C# могут полезны и нам...
наконец увидел что делается с тестами в Проде. Если запустить все тесты, то некоторые методы в некоторых из них валяться.
Но если такой тест запустить там же отдельно, то нормально отрабатывает. Как так?
Плюс, я как то нашел в старом тесте и написал выше вот об этом:
[code]Contact c = new Contact(LastName='test', Email='test@gmail.com', Account=accObject);[/code]
так вот, никакой связи между Контактом и Эккаунто в таком случае вовсе не создается! Создается без-Эккаунтный Контакт...
У тебя отключено параллельное выполнение тестов? У тебя нет @seeAllData в тестах? (хотя пока не понимаю как это может повлиять на твою ситуацию). В остальных случаях тесты ну никак не могут влиять друг на друга.
[quote="Den Brown"]Но если такой тест запустить там же отдельно, то нормально отрабатывает. Как так?[/quote]
У тебя отключено параллельное выполнение тестов?
У тебя нет @seeAllData в тестах? (хотя пока не понимаю как это может повлиять на твою ситуацию).
В остальных случаях тесты ну никак не могут влиять друг на друга.
Может попробуй так?
Contact c = new Contact(LastName='test', Email='test@gmail.com', AccountId=accObject.Id);
и проверь сделал ли ты insert перед этим для accObject.
[quote="Den Brown"]Плюс, я как то нашел в старом тесте и написал выше вот об этом:
Contact c = new Contact(LastName='test', Email='test@gmail.com', Account=accObject);
так вот, никакой связи между Контактом и Эккаунто в таком случае вовсе не создается! Создается без-Эккаунтный Контакт...[/quote]
Может попробуй так?
[code]Contact c = new Contact(LastName='test', Email='test@gmail.com', AccountId=accObject.Id);[/code]
и проверь сделал ли ты insert перед этим для accObject.
с этим все понятною я удивился, что есть еще и "фейковый" вариант этого процесса
[quote="Dmitry Shnyrev"]Contact c = new Contact(LastName='test', Email='test@gmail.com', AccountId=accObject.Id);[/quote]
с этим все понятною я удивился, что есть еще и "фейковый" вариант этого процесса
так, так. помню они там одновременно выполнялись. где эта отключается?
а что при параллельном выполнении меняется? неужели тест дата может смешиваться и быть доступна из разных тестов???
[quote="Dmitry Shnyrev"]У тебя отключено параллельное выполнение тестов? [/quote]
так, так. помню они там одновременно выполнялись. где эта отключается?
а что при параллельном выполнении меняется? неужели тест дата может смешиваться и быть доступна из разных тестов???
Не знаю как подтвердить, но возможно. У меня были проблемы с тестами, но при отключении параллельного выполнения все проходило. Я только могу сказать "мистика"
[quote="Den Brown"]а что при параллельном выполнении меняется? неужели тест дата может смешиваться и быть доступна из разных тестов???[/quote]
Не знаю как подтвердить, но возможно. У меня были проблемы с тестами, но при отключении параллельного выполнения все проходило. Я только могу сказать "мистика"
На счет параллельного выполнения. У меня были проблемы с этим - я использовал Custom Settings. Вот тут и происходила ошибка. Пара тестов пытались писать свои данные в эту одну запись. Вот и валились. Ессесно, если параллелизм выключен, то с этим проблем нет, только со скорость выполнения тестов.
На счет параллельного выполнения.
У меня были проблемы с этим - я использовал Custom Settings. Вот тут и происходила ошибка. Пара тестов пытались писать свои данные в эту одну запись. Вот и валились. Ессесно, если параллелизм выключен, то с этим проблем нет, только со скорость выполнения тестов.
Кстати да, припоминаю. Я редко использую custom settings, но в тех тестах они точно были.
[quote="chiz"]У меня были проблемы с этим - я использовал Custom Settings. [/quote]
Кстати да, припоминаю. Я редко использую custom settings, но в тех тестах они точно были.
Да, кстати, еще один вопрос, который давно вертелся на уме:
во многих тестах создаются Юзеры в порядке создания тест-даты.
Так вот, нужно ли во всех тестах указывать разное уникальное имя пользователя?
у меня могут быть и одинаковые имена в разных тестах, может они фейлят при паралелльном пуске?
Да, кстати, еще один вопрос, который давно вертелся на уме:
во многих тестах создаются Юзеры в порядке создания тест-даты.
Так вот, нужно ли во всех тестах указывать разное уникальное имя пользователя?
у меня могут быть и одинаковые имена в разных тестах, может они фейлят при паралелльном пуске?
Username, как я помню должен быть уникален. А ошибки при параллельном запуске чаще всегдо свзязанны с блокировкой записи.
[quote="Den Brown"]Да, кстати, еще один вопрос, который давно вертелся на уме:
во многих тестах создаются Юзеры в порядке создания тест-даты.
Так вот, нужно ли во всех тестах указывать разное уникальное имя пользователя?
у меня могут быть и одинаковые имена в разных тестах, может они фейлят при паралелльном пуске?[/quote]
Username, как я помню должен быть уникален.
А ошибки при параллельном запуске чаще всегдо свзязанны с блокировкой записи.
Я везде использую одинаковые (тупо шапку тестов копипастю) но опять же повторюсь отключаю параллельное выполнение тестов - проблем не было.
[quote="Den Brown"]Так вот, нужно ли во всех тестах указывать разное уникальное имя пользователя?[/quote]
Я везде использую одинаковые (тупо шапку тестов копипастю) но опять же повторюсь отключаю параллельное выполнение тестов - проблем не было.
Да что вы как дети малые. Ваши тесты НЕ ДОЛЖНЫ выполняться в параллельном режиме. И это уже вы должны сделать причем без всяких чекбоксов в настройках. Или используйте новую фишку setuptest вроде называется.
Да что вы как дети малые. Ваши тесты НЕ ДОЛЖНЫ выполняться в параллельном режиме. И это уже вы должны сделать причем без всяких чекбоксов в настройках. Или используйте новую фишку setuptest вроде называется.
И как ты себе это представляешь? Да, я помню твой способ - запихнуть вызов всех тест методов в один метод и словить лимиты! Круто! Как можно "отключить без чекоксов" параллельное выполнение тестов? Я что-то так и не понял.
[quote="wilder"]Ваши тесты НЕ ДОЛЖНЫ выполняться в параллельном режиме. И это уже вы должны сделать причем без всяких чекбоксов в настройках.[/quote]
И как ты себе это представляешь?
Да, я помню твой способ - запихнуть вызов всех тест методов в один метод и словить лимиты! Круто!
Как можно "отключить без чекоксов" параллельное выполнение тестов? Я что-то так и не понял.
@testSetup все равно посностью не решит проблемы, так как dml могут выполняться и в процессе работы
@testSetup все равно посностью не решит проблемы, так как dml могут выполняться и в процессе работы
Может я чего-то не понимаю, но вот типичный пример из жизни: пакет с тестами в нем 100 тестовых классов в которых по 10 тест методов в каждом тест методе создается пользователь под которым выполняются тесты. Salesforce при включенном параллельном выполнении тестов в идеальном случае может запустить параллельно 1000 методов (если я правильно понимаю параллельно выполняются именно тест методы). И все эти 1000 методов попытаются создать пользователя с один и тем же username. КАК я могу из кода или какой-то архитектурной фишкой заставить все 1000 тестовых методов работать последовательно, по очереди?
Может я чего-то не понимаю, но
вот типичный пример из жизни:
пакет с тестами
в нем 100 тестовых классов в которых по 10 тест методов
в каждом тест методе создается пользователь под которым выполняются тесты.
Salesforce при включенном параллельном выполнении тестов в идеальном случае может запустить параллельно
1000 методов (если я правильно понимаю параллельно выполняются именно тест методы). И все эти 1000 методов попытаются создать пользователя с один и тем же username.
КАК я могу из кода или какой-то архитектурной фишкой заставить все 1000 тестовых методов работать последовательно, по очереди?
Я уже об этом писал и не один раз. Делется один класс в котором делается несколько тест методов, в которые ты собираешь вызовы всех остальных методов тестовых классов.
[quote="Dmitry Shnyrev"]Может я чего-то не понимаю, но
вот типичный пример из жизни:
пакет с тестами
в нем 100 тестовых классов в которых по 10 тест методов
в каждом тест методе создается пользователь под которым выполняются тесты.
Salesforce при включенном параллельном выполнении тестов в идеальном случае может запустить параллельно
1000 методов (если я правильно понимаю параллельно выполняются именно тест методы). И все эти 1000 методов попытаются создать пользователя с один и тем же username.
КАК я могу из кода или какой-то архитектурной фишкой заставить все 1000 тестовых методов работать последовательно, по очереди?[/quote]
Я уже об этом писал и не один раз. Делется один класс в котором делается несколько тест методов, в которые ты собираешь вызовы всех остальных методов тестовых классов.
НО КАК??? Если вызвать 10 тест методов которые кушают по 10 SQL в одном то получим 100 SQL в пределах этого метода. Так? Limit? Или ты знаешь способ как расширить лимиты в пределах ОДНОГО тест метода, который будет вызывать все остальные методы то поделись секретом. КАК Я СМОГУ ЗАПИХНУТЬ вызов 1000 методов в один??????????????????????????
[quote="Dmitry Shnyrev"]И как ты себе это представляешь?
Да, я помню твой способ - запихнуть вызов всех тест методов в один метод и словить лимиты! Круто! [/quote]
НО КАК???
Если вызвать 10 тест методов которые кушают по 10 SQL в одном то получим 100 SQL в пределах этого метода. Так? Limit? Или ты знаешь способ как расширить лимиты в пределах ОДНОГО тест метода, который будет вызывать все остальные методы то поделись секретом.
КАК Я СМОГУ ЗАПИХНУТЬ вызов 1000 методов в один??????????????????????????
Сделай хоть 100 методов, но главное что бы тестовый класс был один. А рамках одного тестового класса все методы выполняются последовательно.
[quote="Dmitry Shnyrev"][quote="Dmitry Shnyrev"]И как ты себе это представляешь?
Да, я помню твой способ - запихнуть вызов всех тест методов в один метод и словить лимиты! Круто! [/quote]
НО КАК???
Если вызвать 10 тест методов которые кушают по 10 SQL в одном то получим 100 SQL в пределах этого метода. Так? Limit? Или ты знаешь способ как расширить лимиты в пределах ОДНОГО тест метода, который будет вызывать все остальные методы то поделись секретом.
КАК Я СМОГУ ЗАПИХНУТЬ вызов 1000 методов в один??????????????????????????[/quote]
Сделай хоть 100 методов, но главное что бы тестовый класс был один. А рамках одного тестового класса все методы выполняются последовательно.
Сделай хоть 100 методов, но главное что бы тестовый класс был один. А рамках одного тестового класса все методы выполняются последовательно.
Как тогда решается проблема параллельного запуска тестов, во время деплоя?
[quote="wilder"]Сделай хоть 100 методов, но главное что бы тестовый класс был один. А рамках одного тестового класса все методы выполняются последовательно.[/quote]
Как тогда решается проблема параллельного запуска тестов, во время деплоя?
А рамках одного тестового класса все методы выполняются последовательно.
Вот это я хотел услышать!!!!!!!!!!!!!!!!! В пределах ОДНОГО класса методы выполняются ПОСЛЕДОВАТЕЛЬНО? Это точно? По моим наблюдениям НЕТ!
[quote="wilder"]А рамках одного тестового класса все методы выполняются последовательно.[/quote]
Вот это я хотел услышать!!!!!!!!!!!!!!!!!
В пределах ОДНОГО класса методы выполняются ПОСЛЕДОВАТЕЛЬНО? Это точно? По моим наблюдениям НЕТ!
А рамках одного тестового класса все методы выполняются последовательно.
Вот это я хотел услышать!!!!!!!!!!!!!!!!! В пределах ОДНОГО класса методы выполняются ПОСЛЕДОВАТЕЛЬНО? Это точно? По моим наблюдениям НЕТ!
Ну, тогда предлагаю проверить У меня в пакете выполняются последовательно.
[quote="Dmitry Shnyrev"][quote="wilder"]А рамках одного тестового класса все методы выполняются последовательно.[/quote]
Вот это я хотел услышать!!!!!!!!!!!!!!!!!
В пределах ОДНОГО класса методы выполняются ПОСЛЕДОВАТЕЛЬНО? Это точно? По моим наблюдениям НЕТ![/quote]
Ну, тогда предлагаю проверить :) У меня в пакете выполняются последовательно.
Оу! То, что создалось в одном методе никак не появляется в другом. Откуда этот прикол, что если что-то создалось в одном методе, то оно будет видно в другом? Сделайте два теста в одном классе и параллельное включите. В одном создайте пользователя с именем ффф1, а в другом ффф2. И пущай вычитают двух пользователей хотя бы в одном методе. Будет по одному в каждом.
Оу!
То, что создалось в одном методе никак не появляется в другом. Откуда этот прикол, что если что-то создалось в одном методе, то оно будет видно в другом?
Сделайте два теста в одном классе и параллельное включите. В одном создайте пользователя с именем ффф1, а в другом ффф2. И пущай вычитают двух пользователей хотя бы в одном методе. Будет по одному в каждом.
У меня тесты в одном классе НЕ выполняются последовательно (при включенном параллелизме). В каждом тесте, у мены вызывается один и тот же метод, который создает тестовый набор данных. Каждый раз, один и тот же, но тесты немного разные для разных случаев. 20 тестов и в каждом я генерирую данные.
У меня тесты в одном классе НЕ выполняются последовательно (при включенном параллелизме).
В каждом тесте, у мены вызывается один и тот же метод, который создает тестовый набор данных. Каждый раз, один и тот же, но тесты немного разные для разных случаев. 20 тестов и в каждом я генерирую данные.
Так вот, нужно ли во всех тестах указывать разное уникальное имя пользователя?
у меня могут быть и одинаковые имена в разных тестах, может они фейлят при паралелльном пуске?
У меня один метод - createTestData(Integer numberOfItems). Я его вызываю во всех тестах. Там прописанны имена объектов. Да, в неготорых местах я использую рандом. Уникальность на имена есть на объектах и ничего не валится при параллельном выполнении тестов.
Единственная проблема - Custom Settings - тут мне пришлось прибегнуть к ОЧЕНЬ плохой практике - делаю upsert объекта в while цикле, пока не создаст или обновит запись :-)
[quote="Den Brown"]Так вот, нужно ли во всех тестах указывать разное уникальное имя пользователя?
у меня могут быть и одинаковые имена в разных тестах, может они фейлят при паралелльном пуске?[/quote]
У меня один метод - createTestData(Integer numberOfItems). Я его вызываю во всех тестах. Там прописанны имена объектов. Да, в неготорых местах я использую рандом. Уникальность на имена есть на объектах и ничего не валится при параллельном выполнении тестов.
Единственная проблема - Custom Settings - тут мне пришлось прибегнуть к ОЧЕНЬ плохой практике - делаю upsert объекта в while цикле, пока не создаст или обновит запись :-)
У меня тесты в одном классе НЕ выполняются последовательно
[quote="chiz"]У меня тесты в одном классе НЕ выполняются последовательно[/quote]
ну тогда плиз доказательства в студию.
Вот мои доказательства обратного.
15:31:44.586 (5586633684)|USER_DEBUG|[83]|ERROR|TestBatchesAndSchedullers 2015-03-11 15:31:41.357
15:31:50.883 (11883047638)|USER_DEBUG|[50]|ERROR|TestClasses 2015-03-11 15:31:47.653
15:35:59.352 (30352514678)|USER_DEBUG|[39]|ERROR|TestTriggers 2015-03-11 15:35:55.971
Более того из этого лога видно что запуск тестов происходит после сортировки по имени. Физически в классе первый метод у меня TestTriggers.
У меня тесты в одном классе НЕ выполняются последовательно
Более того из этого лога видно что запуск тестов происходит после сортировки по имени. Физически в классе первый метод у меня TestTriggers.
Просто вдруг вспомнил, если у вас есть страница и 2 контроллера, и вы передаете параметр на страницу, то вы должны его поймать тем котроллером, у которого имя окажется первым при сортировке, иначе параметр пропадет. Соорри, что не в тему, но вдруг кому-то будет полезно.
[quote="wilder"][quote="chiz"]У меня тесты в одном классе НЕ выполняются последовательно[/quote]
ну тогда плиз доказательства в студию.
Вот мои доказательства обратного.
15:31:44.586 (5586633684)|USER_DEBUG|[83]|ERROR|TestBatchesAndSchedullers 2015-03-11 15:31:41.357
15:31:50.883 (11883047638)|USER_DEBUG|[50]|ERROR|TestClasses 2015-03-11 15:31:47.653
15:35:59.352 (30352514678)|USER_DEBUG|[39]|ERROR|TestTriggers 2015-03-11 15:35:55.971
Более того из этого лога видно что запуск тестов происходит после сортировки по имени. Физически в классе первый метод у меня TestTriggers.[/quote]
Просто вдруг вспомнил, :) если у вас есть страница и 2 контроллера, и вы передаете параметр на страницу, то вы должны его поймать тем котроллером, у которого имя окажется первым при сортировке, иначе параметр пропадет.
Соорри, что не в тему, но вдруг кому-то будет полезно.
Что-то по твоим локам не совсем понятно что один тест метод запускается именно после окончания второго а не во время его работы. Не в смысле ты не прав, а в смысле не понятно из твоего примера. Более того вполне возможно что как раз они запустились параллельно и один тест стартанул быстрее чем другие поэтому и получилось то что имена пошли не в том порядке.
Что-то по твоим локам не совсем понятно что один тест метод запускается именно после окончания второго а не во время его работы. Не в смысле ты не прав, а в смысле не понятно из твоего примера.
Более того вполне возможно что как раз они запустились параллельно и один тест стартанул быстрее чем другие поэтому и получилось то что имена пошли не в том порядке.
Более того вполне возможно что как раз они запустились параллельно и один тест стартанул быстрее чем другие поэтому и получилось то что имена пошли не в том порядке.
Ладно с этим переборщил, но все равно тоже попробую посмотреть этот вопрос по поводу последовательности тестов. Тоже любопытно что там в логах.
[quote="Dmitry Shnyrev"]Более того вполне возможно что как раз они запустились параллельно и один тест стартанул быстрее чем другие поэтому и получилось то что имена пошли не в том порядке.[/quote]
Ладно с этим переборщил, но все равно тоже попробую посмотреть этот вопрос по поводу последовательности тестов. Тоже любопытно что там в логах.
его поймать тем котроллером, у которого имя окажется первым при сортировке
Вот тут ты ошибаешься на сколько я помню из экзамена по DEV501 это зависит от того как они написаны в списке, но не по алфавиту.
[quote="Gres"]его поймать тем котроллером, у которого имя окажется первым при сортировке[/quote]
Вот тут ты ошибаешься :) на сколько я помню из экзамена по DEV501 это зависит от того как они написаны в списке, но не по алфавиту.
Вот тоже провел небольшой эксперимент Погонял 1 тест класс с 3 тест методами.
Пока вынужден согласиться с wilder - запускаются они последовательно (что можно наблюдать по времени вначале строк) и в отсортированном по имени порядке. Будем надеяться что это их нормальное повседневное поведение, которое не изменится при каких-то не зависящих от нас факторов в последующем.
PS. теперь осталось воспроизвести параллельное выполнение двух тест классов для полноты эксперимента.
PPS. Кстати заметил что в MavensMate в списке методы меняют свою последовательность. Сначала обрадовался что уличил wilder но потом по логам понял что последовательность с логами не связана (странно а с чем связана?)
Вот тоже провел небольшой эксперимент
Погонял 1 тест класс с 3 тест методами.
[b]testNotDeletingItem[/b]
14:51:23.538 (2538609195)|USER_DEBUG|[97]|DEBUG|XXXXX03 - start 2015-03-11 13:51:23
14:51:24.912 (3912364286)|USER_DEBUG|[126]|DEBUG|XXXXX03 - end 2015-03-11 13:51:24
[b]testOnAfterInsertAnd[/b]
14:51:25.047 (4047538401)|USER_DEBUG|[35]|DEBUG|XXXXX01 - start 2015-03-11 13:51:25
14:51:25.269 (4269406679)|USER_DEBUG|[51]|DEBUG|XXXXX01 - end 2015-03-11 13:51:25
[b]testOnBeforeDelete[/b]
14:51:25.345 (4345669295)|USER_DEBUG|[58]|DEBUG|XXXXX02 - start 2015-03-11 13:51:25
14:51:25.846 (4846772279)|USER_DEBUG|[90]|DEBUG|XXXXX02 - end 2015-03-11 13:51:25
Пока вынужден согласиться с wilder - запускаются они последовательно (что можно наблюдать по времени вначале строк) и в отсортированном по имени порядке.
Будем надеяться что это их нормальное повседневное поведение, которое не изменится при каких-то не зависящих от нас факторов в последующем.
PS. теперь осталось воспроизвести параллельное выполнение двух тест классов для полноты эксперимента.
PPS. Кстати заметил что в MavensMate в списке методы меняют свою последовательность. Сначала обрадовался что уличил wilder но потом по логам понял что последовательность с логами не связана (странно а с чем связана?)
его поймать тем котроллером, у которого имя окажется первым при сортировке
Вот тут ты ошибаешься на сколько я помню из экзамена по DEV501 это зависит от того как они написаны в списке, но не по алфавиту.
Прости, но я проверил на практике :), тоже думал, что они должны загружаться в той последовательности в, которой написаны.
[quote="wilder"][quote="Gres"]его поймать тем котроллером, у которого имя окажется первым при сортировке[/quote]
Вот тут ты ошибаешься :) на сколько я помню из экзамена по DEV501 это зависит от того как они написаны в списке, но не по алфавиту.[/quote]
Прости, но я проверил на практике :), тоже думал, что они должны загружаться в той последовательности в, которой написаны.
его поймать тем котроллером, у которого имя окажется первым при сортировке
Вот тут ты ошибаешься на сколько я помню из экзамена по DEV501 это зависит от того как они написаны в списке, но не по алфавиту.
[quote="wilder"][quote="Gres"]его поймать тем котроллером, у которого имя окажется первым при сортировке[/quote]
Вот тут ты ошибаешься :) на сколько я помню из экзамена по DEV501 это зависит от того как они написаны в списке, но не по алфавиту.[/quote]
А ты сдавал 501?
его поймать тем котроллером, у которого имя окажется первым при сортировке
Вот тут ты ошибаешься на сколько я помню из экзамена по DEV501 это зависит от того как они написаны в списке, но не по алфавиту.
[quote="Gres"][quote="wilder"][quote="Gres"]его поймать тем котроллером, у которого имя окажется первым при сортировке[/quote]
Вот тут ты ошибаешься :) на сколько я помню из экзамена по DEV501 это зависит от того как они написаны в списке, но не по алфавиту.[/quote]
А ты сдавал 501?[/quote]
Я нет :) я просто видел часть вопросов.
[quote="Gres"][quote="wilder"]Я нет я просто видел часть вопросов.[/quote]
Будет время, попробуй, если интересно.[/quote]
Если честно, не интересно. Разочаровался я в этих бумажках :(
Если честно, не интересно. Разочаровался я в этих бумажках :(
[quote="wilder"]Если честно, не интересно. Разочаровался я в этих бумажках :([/quote]
Я не про бумажку, я про кейс.
Насчет бумажек я с вами полностью солидарен.
Разрешите мне вставиться в вашу беседу со своими нехитрыми проблемами.
- тест ранится нормально в свежайшей фулл-копии прода и фейлится в проде на "mixed DML" (в обоих случаях был одиночный пуск внутри Орга).
- другой тест ранится нормально в орге, если запустить его с Эклипса, и фейлится на "mixed DML" при одиночном пуске внутри этого же Орга.
- также не понимаю механизма, почему при последовательном пуске пара тестов фейлится в Проде, но при попытке деплоя нового чендж сета никаких ошибок не выходит?
Разрешите мне вставиться в вашу беседу со своими нехитрыми проблемами.
- тест ранится нормально в свежайшей фулл-копии прода и фейлится в проде на "mixed DML" (в обоих случаях был одиночный пуск внутри Орга).
- другой тест ранится нормально в орге, если запустить его с Эклипса, и фейлится на "mixed DML" при одиночном пуске внутри этого же Орга.
- также не понимаю механизма, почему при последовательном пуске пара тестов фейлится в Проде, но при попытке деплоя нового чендж сета никаких ошибок не выходит?
[quote="wilder"]ну тогда плиз доказательства в студию.[/quote]
Я в дев консоли тесты запускаю. Действительно, отсортировано по имени запускаются. Жесть :-)
Разрешите мне вставиться в вашу беседу со своими нехитрыми проблемами.
- тест ранится нормально в свежайшей фулл-копии прода и фейлится в проде на "mixed DML" (в обоих случаях был одиночный пуск внутри Орга).
- другой тест ранится нормально в орге, если запустить его с Эклипса, и фейлится на "mixed DML" при одиночном пуске внутри этого же Орга.
- также не понимаю механизма, почему при последовательном пуске пара тестов фейлится в Проде, но при попытке деплоя нового чендж сета никаких ошибок не выходит?
[quote="Den Brown"]Разрешите мне вставиться в вашу беседу со своими нехитрыми проблемами.
- тест ранится нормально в свежайшей фулл-копии прода и фейлится в проде на "mixed DML" (в обоих случаях был одиночный пуск внутри Орга).
- другой тест ранится нормально в орге, если запустить его с Эклипса, и фейлится на "mixed DML" при одиночном пуске внутри этого же Орга.
- также не понимаю механизма, почему при последовательном пуске пара тестов фейлится в Проде, но при попытке деплоя нового чендж сета никаких ошибок не выходит?[/quote]
Шикарно!
Ты читал http://stackoverflow.com/questions/2387475/how-to-avoid-mixed-dml-operation-error-in-salesforce-tests-that-create-users ? Там вроде разобрались с этой проблемой.
Фигассе, я такого и не знал.
http://www.salesforce.com/us/developer/docs/apexcode/index_Left.htm#StartTopic=Content/apex_dml_non_mix_sobjects.htm?SearchType
Как полезно бывать на форумах :-)
Как полезно бывать на форумах :-)
На форумах очень полезно бывать. Я Очень много интересного тут узнал. Как показала практика люди в одной компании обычно развиваются в одном направлении и многие аспекты просто не захватывают. А тут народ из разных контор, с разным опытом. Это реально круто. Не понимаю своих бывших коллег, которые говорят что тут делать нечего, потому что тут одни школьники сидят. Ну пусть так дальше думают.
[quote="chiz"]Как полезно бывать на форумах :-)[/quote]
На форумах очень полезно бывать.
Я Очень много интересного тут узнал.
Как показала практика люди в одной компании обычно развиваются в одном направлении и многие аспекты просто не захватывают. А тут народ из разных контор, с разным опытом. Это реально круто. Не понимаю своих бывших коллег, которые говорят что тут делать нечего, потому что тут одни школьники сидят. Ну пусть так дальше думают.
Кстати, я тут в соседней теме про сертификаты увидел высказывание, что до использования System.assertEquals() доходят не многие. А как тогда тестировать свой код? Как без этих проверок?
Кстати, я тут в соседней теме про сертификаты увидел высказывание, что до использования System.assertEquals() доходят не многие. А как тогда тестировать свой код? Как без этих проверок?
А как тогда тестировать свой код? Как без этих проверок?
А многие не тестируют код, а покрывают тестами А для этого assertEquals не нужны
[quote="chiz"]А как тогда тестировать свой код? Как без этих проверок?[/quote]
А многие не тестируют код, а покрывают тестами :) А для этого assertEquals не нужны :) :) :)
[quote="Chiz"]Ты читал http://stackoverflow.com/questions/2387475/how-to-avoid-mixed-dml-operation-error-in-salesforce-tests-that-create-users ? Там вроде разобрались с этой проблемой.[/quote]
вот это надо бы попробовать, надеюсь поможет.
Я-то сталкивался, да очень редко. Я, обычно, сам работаю. Ну как, СФ программер я один. И тесты пишу я. И тесты я пишу так, чтоб проверить функционал без кликания мышкой в браузере или в приложениях. Я ооочень ленивый. Я пару раз видел инициализацию класса (контроллера в том случае) и вызовы метода с передачей параметров типа new sObject__c(field__c = 'qwerty'). Но это пару печальных случаев, кот перешли мне по наследству. А так... Надо искать какие-то шабашки, где больше одного СФ программера, чтоб было весело. А то я в своем мирке пидалю, а люди вон как делают :-)
Я-то сталкивался, да очень редко. Я, обычно, сам работаю. Ну как, СФ программер я один. И тесты пишу я. И тесты я пишу так, чтоб проверить функционал без кликания мышкой в браузере или в приложениях. Я ооочень ленивый.
Я пару раз видел инициализацию класса (контроллера в том случае) и вызовы метода с передачей параметров типа new sObject__c(field__c = 'qwerty'). Но это пару печальных случаев, кот перешли мне по наследству. А так...
Надо искать какие-то шабашки, где больше одного СФ программера, чтоб было весело. А то я в своем мирке пидалю, а люди вон как делают :-)
А то я в своем мирке пидалю, а люди вон как делают :-)
Аналогично) Только нас двое) Смотрю я иногда что я нашКодил когда только начинал работать, аш жутко становится(
[quote="Chiz"] А то я в своем мирке пидалю, а люди вон как делают :-)[/quote]
Аналогично) Только нас двое) Смотрю я иногда что я нашКодил когда только начинал работать, аш жутко становится(
Do you even assert, bro?
System.assert( System.Limits.getQueries() < System.Limits.getLimitQueries()/10 , 'SOQL consumption ('+System.Limits.getQueries()+':'+System.Limits.getLimitQueries()+') is too damn high! ' );
Do you even assert, bro?
[code]System.assert(
System.Limits.getQueries() < System.Limits.getLimitQueries()/10
, 'SOQL consumption ('+System.Limits.getQueries()+':'+System.Limits.getLimitQueries()+') is too damn high! '
);[/code]
Но мне кажется, в прод такое не пустят = ))
Как то уж слишком эта проверка сурово выглядит. В salesforce и так количество SOQL запросов ограничено (весь остальной dev мир над нами смеется, хотя мы то понимаем, что это круто), а ты еще на в 10 раз хочешь это дело сократить. Это должно быть в виде Warning но никак не на уровне тестов.
Как то уж слишком эта проверка сурово выглядит. В salesforce и так количество SOQL запросов ограничено (весь остальной dev мир над нами смеется, хотя мы то понимаем, что это круто), а ты еще на в 10 раз хочешь это дело сократить.
Это должно быть в виде Warning но никак не на уровне тестов.
Как то уж слишком эта проверка сурово выглядит. В salesforce и так количество SOQL запросов ограничено (весь остальной dev мир над нами смеется, хотя мы то понимаем, что это круто), а ты еще на в 10 раз хочешь это дело сократить. Это должно быть в виде Warning но никак не на уровне тестов.
Я тут недавно тупанул с функционалом и на проде выстрелило. По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста. У меня такого объема нет в песочнице, вот у меня и не выстрелило. А у меня с тестовым набором, который я могу сгенерировать до test.starttest() хватает максимум на два SOQL. А с тем кодом было около двадцати. Так что я себя обе запрашиваю.
[quote="Dmitry Shnyrev"]Как то уж слишком эта проверка сурово выглядит. В salesforce и так количество SOQL запросов ограничено (весь остальной dev мир над нами смеется, хотя мы то понимаем, что это круто), а ты еще на в 10 раз хочешь это дело сократить.
Это должно быть в виде Warning но никак не на уровне тестов.[/quote]
Я тут недавно тупанул с функционалом и на проде выстрелило. По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста. У меня такого объема нет в песочнице, вот у меня и не выстрелило. А у меня с тестовым набором, который я могу сгенерировать до test.starttest() хватает максимум на два SOQL. А с тем кодом было около двадцати. Так что я себя обе запрашиваю.
По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста.
Вот это кстати большая проблема архитектуры кода. Количество SOQL запросов должно быть постоянным и не зависеть от количества данных. Значит где-то допущена грубейшая ошибка и SOQL попал в for.
[quote="Chiz"]По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста.[/quote]
Вот это кстати большая проблема архитектуры кода.
Количество SOQL запросов должно быть постоянным и не зависеть от количества данных. Значит где-то допущена грубейшая ошибка и SOQL попал в for.
По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста.
Вот это кстати большая проблема архитектуры кода. Количество SOQL запросов должно быть постоянным и не зависеть от количества данных. Значит где-то допущена грубейшая ошибка и SOQL попал в for.
Тупить надо меньше = )) Кстати да, в фор. Но подвох в том, что в форе создавался новый ДТО. И я в этот ДТО засунул запросы. Когда мне написали, что на проде вывалились ошибки, меня чуть сердце не остановилось. Это уловка для таких тупняков, которые на меня могут находить. Это было ужасно.
[quote="Dmitry Shnyrev"][quote="Chiz"]По-хорошему, метод должен съесть не более двух запросов, а он съел больше ста.[/quote]
Вот это кстати большая проблема архитектуры кода.
Количество SOQL запросов должно быть постоянным и не зависеть от количества данных. Значит где-то допущена грубейшая ошибка и SOQL попал в for.[/quote]
Тупить надо меньше = ))
Кстати да, в фор. Но подвох в том, что в форе создавался новый ДТО. И я в этот ДТО засунул запросы. Когда мне написали, что на проде вывалились ошибки, меня чуть сердце не остановилось.
Это уловка для таких тупняков, которые на меня могут находить. Это было ужасно.
Решение есть, но оно работает только в одном случае и распространяется только на код за который вы несёте ответственость и/или к которому имеете доступ. По хорошему каждый 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 не должен зависеть от других unit test'ов и триггер соответственно от другого триггера, поэтому надо предусмотреть в триггере механизм позволяющий отключить его исполнение, банально через статическую переменную, например так (код показан в целях примера и не идеален):
[code]//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
}
}
[/code]
Таким образом ваши триггера не подложат свинью другим, а за то что кто-то другой не приведёт к такой ситуации вы отвечать не можете.
А разве отключение триггеров в тестах не портит весь смысл тестирования? Если триггер конфликтуют между собой, то не лучше ли выявить это на этапе тестирования, а не в процессе использования? Если ваш триггер может подложить свинью другим это ли не повод узнать это в тестах?
А разве отключение триггеров в тестах не портит весь смысл тестирования?
Если триггер конфликтуют между собой, то не лучше ли выявить это на этапе тестирования, а не в процессе использования? Если ваш триггер может подложить свинью другим это ли не повод узнать это в тестах?
А разве отключение триггеров в тестах не портит весь смысл тестирования? Если триггер конфликтуют между собой, то не лучше ли выявить это на этапе тестирования, а не в процессе использования? Если ваш триггер может подложить свинью другим это ли не повод узнать это в тестах?
Проблема - "возрастание лимитной нагрузки" - предложен инструмент для её решения. Как пользоваться этим инструментом и пользоваться ли им вообще - это уже другой вопрос.
И если уж на то пошло - unit test как раз и должен тестировать отдельный кусочек, а как всё работает вместе, это уже кажись интеграционное тестирование.
[quote="Dmitry Shnyrev"]А разве отключение триггеров в тестах не портит весь смысл тестирования?
Если триггер конфликтуют между собой, то не лучше ли выявить это на этапе тестирования, а не в процессе использования? Если ваш триггер может подложить свинью другим это ли не повод узнать это в тестах?[/quote]
Проблема - "возрастание лимитной нагрузки" - предложен инструмент для её решения. Как пользоваться этим инструментом и пользоваться ли им вообще - это уже другой вопрос.
И если уж на то пошло - unit test как раз и должен тестировать отдельный кусочек, а как всё работает вместе, это уже кажись интеграционное тестирование.
Кстати, да; я юнит тесты частенько не разделяю с интеграционными. У меня, по сути, и нет юнит тестов - одни интеграционные. Надо исправляться. Полезно читать форум :-)
Кстати, да; я юнит тесты частенько не разделяю с интеграционными. У меня, по сути, и нет юнит тестов - одни интеграционные.
Надо исправляться.
Полезно читать форум :-)
Кстати, да; я юнит тесты частенько не разделяю с интеграционными. У меня, по сути, и нет юнит тестов - одни интеграционные.
Согласен, у меня наверное тоже самое. Ну это исторически сложилось на Salesforce. Теорию по тестированию не изучаем, а покрытие 75% сделать надо. На других платформах наверное когда дело доходит до тестов явно тема начинается с изучения теории.
[quote="Chiz"]Кстати, да; я юнит тесты частенько не разделяю с интеграционными. У меня, по сути, и нет юнит тестов - одни интеграционные. [/quote]
Согласен, у меня наверное тоже самое. Ну это исторически сложилось на Salesforce. Теорию по тестированию не изучаем, а покрытие 75% сделать надо. На других платформах наверное когда дело доходит до тестов явно тема начинается с изучения теории.
Таким образом ваши триггера не подложат свинью другим, а за то что кто-то другой не приведёт к такой ситуации вы отвечать не можете.
Триггер не должен знать ничего о тестах, исходя из Single responsibility principle
[quote="ilya leshchuk"]Таким образом ваши триггера не подложат свинью другим, а за то что кто-то другой не приведёт к такой ситуации вы отвечать не можете.[/quote]
Триггер не должен знать ничего о тестах, исходя из Single responsibility principle
Таким образом ваши триггера не подложат свинью другим, а за то что кто-то другой не приведёт к такой ситуации вы отвечать не можете.
Триггер не должен знать ничего о тестах, исходя из Single responsibility principle
Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.
Во-вторых - не вижу чем это противоречит single responsibility principle? Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
[quote="Gres"][quote="ilya leshchuk"]Таким образом ваши триггера не подложат свинью другим, а за то что кто-то другой не приведёт к такой ситуации вы отвечать не можете.[/quote]
Триггер не должен знать ничего о тестах, исходя из Single responsibility principle[/quote]
Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.
Во-вторых - не вижу чем это противоречит single responsibility principle?
Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.
В итоге получаем в триггере 100500 if только для тестов.
Во-вторых - не вижу чем это противоречит single responsibility principle?
Написал же, триггер знает о том, что существуют тесты.
Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
Я же просто высказал свое мнение. Тут большинство людей пишут все в одном классе со 100500 конструкциями if.
[quote="ilya leshchuk"]Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.[/quote]
В итоге получаем в триггере 100500 if только для тестов.
[quote="ilya leshchuk"]Во-вторых - не вижу чем это противоречит single responsibility principle? [/quote]
Написал же, триггер знает о том, что существуют тесты.
[quote="ilya leshchuk"]Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.[/quote]
Я же просто высказал свое мнение. Тут большинство людей пишут все в одном классе со 100500 конструкциями if.
Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.
В итоге получаем в триггере 100500 if только для тестов.
If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).
Во-вторых - не вижу чем это противоречит single responsibility principle?
Написал же, триггер знает о том, что существуют тесты.
Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.
Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
Я же просто высказал свое мнение. Тут большинство людей пишут все в одном классе со 100500 конструкциями if.
[quote="Gres"][quote="ilya leshchuk"]Да ради бога, я же написал код для примера - переменную таким образом инициализировать удобнее с точки зрения что триггер не надо будет выключать в тестах специально во всех 100500 местах, 100400 из которых в унаследованном коде, его надо будет включить там где вы захотите его протестировать.[/quote]
В итоге получаем в триггере 100500 if только для тестов.
[/quote]
If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).
[quote="Gres"]
[quote="ilya leshchuk"]Во-вторых - не вижу чем это противоречит single responsibility principle? [/quote]
Написал же, триггер знает о том, что существуют тесты.
[/quote]
Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.
[quote="Gres"]
[quote="ilya leshchuk"]Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.[/quote]
Я же просто высказал свое мнение. Тут большинство людей пишут все в одном классе со 100500 конструкциями if.[/quote]
Про 100500 конструкций if смотрите выше.
If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).
Ну смотрите, вы говорите, что можете контролировать выполнение определенной логики в триггере. А разве их не может быть несколько? Просто разделение триггера на "мой" и "чужой" код выглядит не очень красиво.
[quote="ilya leshchuk"]If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).[/quote]
Ну смотрите, вы говорите, что можете контролировать выполнение определенной логики в триггере. А разве их не может быть несколько? Просто разделение триггера на "мой" и "чужой" код выглядит не очень красиво.
Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.
Давайте обсудим, не правы как раз вы, суть принципов SOLID как раз в том, чтобы сделать архитектуру гибкой, для этого нужно использовать абстракции. Класс не должен знать о конкретной реализации других классов. Реализации в него нужно инжектить в виде абстракций. Tests.isTestRunning() - я считаю костылем. Использование этой конструкции подразумевает то, что у вас есть проблемы с покрытием класса тестами, а это явная ошибка архитектуры.
[quote="ilya leshchuk"]Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.[/quote]
Давайте обсудим, не правы как раз вы, суть принципов SOLID как раз в том, чтобы сделать архитектуру гибкой, для этого нужно использовать абстракции. Класс не должен знать о конкретной реализации других классов. Реализации в него нужно инжектить в виде абстракций.
Tests.isTestRunning() - я считаю костылем. Использование этой конструкции подразумевает то, что у вас есть проблемы с покрытием класса тестами, а это явная ошибка архитектуры.
If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).
Ну смотрите, вы говорите, что можете контролировать выполнение определенной логики в триггере. А разве их не может быть несколько? Просто разделение триггера на "мой" и "чужой" код выглядит не очень красиво.
Не знаю упоминалось ли здесь на форуме, уверен что да, что почти каждый кто работет с форсом, рано или поздно, приходит к шаблону некоего диспатчера для триггера, который в зависимости от события передаёт управление в те или иные хелперы, сервисы и т.п. Предложенный подход как раз и предлагает выключать этот диспатчер и метод даже назван dispatch чтобы натолкнуть на эту мысль. Сами же хелперы, сервисы и прочее, тестируются в своих собственных unit test'ах. А вот дальше уже и идёт разделение ответсвтенности - я отвечаю за "мою" часть бизнес процесса, а другой разработчик за "свою".
Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.
Давайте обсудим, не правы как раз вы, суть принципов SOLID как раз в том, чтобы сделать архитектуру гибкой, для этого нужно использовать абстракции. Класс не должен знать о конкретной реализации других классов. Реализации в него нужно инжектить в виде абстракций. Tests.isTestRunning() - я считаю костылем. Использование этой конструкции подразумевает то, что у вас есть проблемы с покрытием класса тестами, а это явная ошибка архитектуры.
Я не против, это же был пример, никто не мешает вам завести абсракцию - например фабрику триггеров, которая вам в зависимости от контекста будет подсовывать соответствующий триггер/диспатчер, а для контекста test'а подсовывать например null object'овый класс-болванку, который ничего реально не делает. Единственное что есть в том числе и замечательный принцип KISS - зачем усложнять архитектуру без надобности, а чтобы появилась надобность надо чтобы был хоть один случай или ситуация, причём реальная, а не теоретическая, в которой предложенный подход не работает или увеличивает сложность.
[quote="Gres"][quote="ilya leshchuk"]If по-идее должен быть только один на весь ApexTrigger класс, т.к. для этого нужно использовать одну точку входа, где как раз и будет стоять этот if (один а не 100500).[/quote]
Ну смотрите, вы говорите, что можете контролировать выполнение определенной логики в триггере. А разве их не может быть несколько? Просто разделение триггера на "мой" и "чужой" код выглядит не очень красиво.[/quote]
Не знаю упоминалось ли здесь на форуме, уверен что да, что почти каждый кто работет с форсом, рано или поздно, приходит к шаблону некоего диспатчера для триггера, который в зависимости от события передаёт управление в те или иные хелперы, сервисы и т.п. Предложенный подход как раз и предлагает выключать этот диспатчер и метод даже назван dispatch чтобы натолкнуть на эту мысль. Сами же хелперы, сервисы и прочее, тестируются в своих собственных unit test'ах. А вот дальше уже и идёт разделение ответсвтенности - я отвечаю за "мою" часть бизнес процесса, а другой разработчик за "свою".
[quote="Gres"][quote="ilya leshchuk"]Я думаю что вы неверно понимаете этот принцип - по вашей логике получается что я в своём классе не могу использовать другие классы? И по этой же логике получается, что метод Tests.isTestRunning() не имеет смысла, потому что в тесте мы и так знаем где мы находимся, а за пределами теста нам не позволяет его использовать single responsibility principle.[/quote]
Давайте обсудим, не правы как раз вы, суть принципов SOLID как раз в том, чтобы сделать архитектуру гибкой, для этого нужно использовать абстракции. Класс не должен знать о конкретной реализации других классов. Реализации в него нужно инжектить в виде абстракций.
Tests.isTestRunning() - я считаю костылем. Использование этой конструкции подразумевает то, что у вас есть проблемы с покрытием класса тестами, а это явная ошибка архитектуры.[/quote]
Я не против, это же был пример, никто не мешает вам завести абсракцию - например фабрику триггеров, которая вам в зависимости от контекста будет подсовывать соответствующий триггер/диспатчер, а для контекста test'а подсовывать например null object'овый класс-болванку, который ничего реально не делает. Единственное что есть в том числе и замечательный принцип KISS - зачем усложнять архитектуру без надобности, а чтобы появилась надобность надо чтобы был хоть один случай или ситуация, причём реальная, а не теоретическая, в которой предложенный подход не работает или увеличивает сложность.
KISS - зачем усложнять архитектуру без надобности, а чтобы появилась надобность надо чтобы был хоть один случай или ситуация, причём реальная, а не теоретическая, в которой предложенный подход не работает или увеличивает сложность.
Что в данном случае понимается под усложнением? Выполнение триггера может контролироваться в том числе и кастомными настройками, запущенным батчем и т.д. Вполне реальный пример? Я просто предлагаю инжектить это управление, как зависимость от текущего контекста.
Не знаю упоминалось ли здесь на форуме, уверен что да, что почти каждый кто работет с форсом, рано или поздно, приходит к шаблону некоего диспатчера для триггера, который в зависимости от события передаёт управление в те или иные хелперы, сервисы и т.п. Предложенный подход как раз и предлагает выключать этот диспатчер и метод даже назван dispatch чтобы натолкнуть на эту мысль. Сами же хелперы, сервисы и прочее, тестируются в своих собственных unit test'ах. А вот дальше уже и идёт разделение ответсвтенности - я отвечаю за "мою" часть бизнес процесса, а другой разработчик за "свою".
Вполне с этим согласен и разделение ответственности применимо не только к триггерам.
[quote="ilya leshchuk"]KISS - зачем усложнять архитектуру без надобности, а чтобы появилась надобность надо чтобы был хоть один случай или ситуация, причём реальная, а не теоретическая, в которой предложенный подход не работает или увеличивает сложность.[/quote]
Что в данном случае понимается под усложнением?
Выполнение триггера может контролироваться в том числе и кастомными настройками, запущенным батчем и т.д.
Вполне реальный пример?
Я просто предлагаю инжектить это управление, как зависимость от текущего контекста.
[quote="ilya leshchuk"]Не знаю упоминалось ли здесь на форуме, уверен что да, что почти каждый кто работет с форсом, рано или поздно, приходит к шаблону некоего диспатчера для триггера, который в зависимости от события передаёт управление в те или иные хелперы, сервисы и т.п. Предложенный подход как раз и предлагает выключать этот диспатчер и метод даже назван dispatch чтобы натолкнуть на эту мысль. Сами же хелперы, сервисы и прочее, тестируются в своих собственных unit test'ах. А вот дальше уже и идёт разделение ответсвтенности - я отвечаю за "мою" часть бизнес процесса, а другой разработчик за "свою".[/quote]
Вполне с этим согласен и разделение ответственности применимо не только к триггерам.
Очень интересная дискуссия. Говорю же, много интересных людей прибавилось последнее время.
Итак: не всегда наши ЮТ должны быть интеграционными, в случае когда доходит до проблем с лимитами, можно и отключить часть "дополнительной" логики. Правда в таком случае возникает вопрос, а кто будет писать интеграционный тест? Но с другой стороны если писать интеграционный тест, то в нем уже не гонешься за покрытием твоего кода, то есть в нем меньше логики можно вызвать, а значит проблем с лимитами может и не быть, ведь нужно просто "прозвонить" всю цепь событий.
А для отключения тригера в его сервис-классе(-сах) можно предусмотреть стат переменную. Но чтобы это действительно предварилось в жизнь, нужно делать требования ко всем тригерам иметь такую переменную...
Очень интересная дискуссия. Говорю же, много интересных людей прибавилось последнее время.
Итак: не всегда наши ЮТ должны быть интеграционными, в случае когда доходит до проблем с лимитами, можно и отключить часть "дополнительной" логики. Правда в таком случае возникает вопрос, а кто будет писать интеграционный тест? Но с другой стороны если писать интеграционный тест, то в нем уже не гонешься за покрытием твоего кода, то есть в нем меньше логики можно вызвать, а значит проблем с лимитами может и не быть, ведь нужно просто "прозвонить" всю цепь событий.
А для отключения тригера в его сервис-классе(-сах) можно предусмотреть стат переменную. Но чтобы это действительно предварилось в жизнь, нужно делать требования ко всем тригерам иметь такую переменную...
Гоняться хуже, чем "прозванивать" Тем более при прозвонке большая часть и так покрывается. Ну и да, часто вижу их отсутствие, но не надо забывать про assert'ы. И да, привет, Илья)
Гоняться хуже, чем "прозванивать"
Тем более при прозвонке большая часть и так покрывается. Ну и да, часто вижу их отсутствие, но не надо забывать про assert'ы.
И да, привет, Илья)
//Trigger code trigger onAccount on Account (before insert, ...) { new AccountTrigger().dispatch(); }
постойте-ка, получается что все что есть в таком тригере - это одна строка инициализации класса и вызов метода (или вызов стат метода). понятно, что вся логика из тригера выводится в сервис слой, но здесь в тригере даже нет "разводки" логики по наВставку, наАпдейт, До, После. Т.е. абсолютно все делается в классе. А все контекстные переменные тригера подаются в аргументы конструктора класса или в метод. Или их и подавать в класс не нужно так как они будут доступны внутри метода (например как Trigger.new)???
И что нам дает такой "пустой" тригер с полностью выведенной логикой?
Во-первых, такой тригер покрыть тестом - как два пальца об асфальт: любая вставка записи покроет эту единственную строку.
Во-вторых, если мы все-таки передаем все контекстные переменные тригера в аргументы нашего класса, то получается, что для тестирования моего класса мне вовсе не обязательно, что-то куда-то ДМЛить: достаточно просто создать несколько объектов и подать их в конструктор или метод.
А что это нам дает? Ну например, полная модульность: если нужно добавить на объект новую логику, достаточно в тригере просто добавить еще одну строку с вывозом нового класса\метода.
[quote="ilya leshchuk"]//Trigger code
trigger onAccount on Account (before insert, ...) {
new AccountTrigger().dispatch();
}[/quote]
постойте-ка, получается что все что есть в таком тригере - это одна строка инициализации класса и вызов метода (или вызов стат метода). понятно, что вся логика из тригера выводится в сервис слой, но здесь в тригере даже нет "разводки" логики по наВставку, наАпдейт, До, После. Т.е. абсолютно все делается в классе. А все контекстные переменные тригера подаются в аргументы конструктора класса или в метод. Или их и подавать в класс не нужно так как они будут доступны внутри метода (например как Trigger.new)???
И что нам дает такой "пустой" тригер с полностью выведенной логикой?
Во-первых, такой тригер покрыть тестом - как два пальца об асфальт: любая вставка записи покроет эту единственную строку.
Во-вторых, если мы все-таки передаем все контекстные переменные тригера в аргументы нашего класса, то получается, что для тестирования моего класса мне вовсе не обязательно, что-то куда-то ДМЛить: достаточно просто создать несколько объектов и подать их в конструктор или метод.
А что это нам дает? Ну например, полная модульность: если нужно добавить на объект новую логику, достаточно в тригере просто добавить еще одну строку с вывозом нового класса\метода.
Я правильно думаю?
Ты все правильно думаешь. Так же это позволяет нам собирать статистику о вложенности триггеров разрултвать ситуации с зацикливаниями и много что еще другого.
Ты все правильно думаешь. Так же это позволяет нам собирать статистику о вложенности триггеров разрултвать ситуации с зацикливаниями и много что еще другого.
[quote="Gres"]Нужно просто любить ООП (:[/quote]
не важно, любишь ли ты ООП или нет.
важно что ООП любит всех нас и тебя персонально.
и рано или поздно ты придешь к нему...
так все-таки, вы в свой "тригерный" класс\метод передаете контекстный переменные тригера как аргументы?
и рано или поздно ты придешь к нему...
Или не придешь
Можно спокойно работать с функциями (статических методов, объединенных в сервис классы по смыслу) и мапами <String, Object> вместо DTO
[quote="Den Brown"]и рано или поздно ты придешь к нему...[/quote]
Или не придешь :)
Можно спокойно работать с функциями (статических методов, объединенных в сервис классы по смыслу)
и мапами <String, Object> вместо DTO :D
Все зависит от воспитания и среды обитания.
Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
[quote="ilya leshchuk"]Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.[/quote]
Очень понравилась эта фраза! Запомню!
Очень понравилась эта фраза! Запомню
Это не оправдание никогда их не использовать. Тут скорее речь о целесообразности. Эту же фразу можно применить к любым best practices.
[quote="Dmitry Shnyrev"]Очень понравилась эта фраза! Запомню[/quote]
Это не оправдание никогда их не использовать. Тут скорее речь о целесообразности.
Эту же фразу можно применить к любым best practices.
Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.
Очень понравилась эта фраза! Запомню!
Есть даже такой антипаттерн - одержимость примитивами.
[quote="Dmitry Shnyrev"][quote="ilya leshchuk"]Ну и в-третьих - паттерны это не must, это should - не требуется безприкословное их исполнение.[/quote]
Очень понравилась эта фраза! Запомню![/quote]
Есть даже такой антипаттерн - одержимость примитивами.
Нужно просто любить ООП (:
не важно, любишь ли ты ООП или нет.
важно что ООП любит всех нас и тебя персонально.
и рано или поздно ты придешь к нему...
так все-таки, вы в свой "тригерный" класс\метод передаете контекстный переменные тригера как аргументы?
Выкладываю свой абстрактный класс, который использую для таких диспатчеров. Заодно оговорюсь - писался он давно, тесты содержит в себе самом (тогда так можно было и что бы там кто ни говорил - мне это нравилось, потому как не плодилось стопятьсот файлов и был дополнительный стимул делать классы компактными, чтобы даже с тестами они занимали разумное кол-во строк), 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-методы.
[quote="Den Brown"][quote="Gres"]Нужно просто любить ООП (:[/quote]
не важно, любишь ли ты ООП или нет.
важно что ООП любит всех нас и тебя персонально.
и рано или поздно ты придешь к нему...
так все-таки, вы в свой "тригерный" класс\метод передаете контекстный переменные тригера как аргументы?[/quote]
Выкладываю свой абстрактный класс, который использую для таких диспатчеров. Заодно оговорюсь - писался он давно, тесты содержит в себе самом (тогда так можно было и что бы там кто ни говорил - мне это нравилось, потому как не плодилось стопятьсот файлов и был дополнительный стимул делать классы компактными, чтобы даже с тестами они занимали разумное кол-во строк), global стоит т.к. класс входил в часть managed package, так что смело можно на public заменить. Со времени своего написания этой архитектуры хватало на 100% случаев с которыми приходилось с тех пор столкнуться.
[code]
/*
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()
{
try
{
if (Trigger.isExecuting == true || ALLOW_EXECUTION == true)
{
hookInitialize();
if (Trigger.isBefore == true)
{
onBefore();
}
else if (Trigger.isAfter == true)
{
onAfter();
}
hookFinalize();
}
}
catch (Exception e)
{
hookException(e);
}
}
protected void onBefore()
{
if (Trigger.isDelete == true)
{
onBeforeDelete(Trigger.old);
}
else if (Trigger.isUpdate == true)
{
onBeforeUpdate(Trigger.new, Trigger.old);
}
else if (Trigger.isInsert == true)
{
onBeforeInsert(Trigger.new);
}
}
protected void onAfter()
{
if (Trigger.isDelete == true)
{
onAfterDelete(Trigger.old);
}
else if (Trigger.isUpdate == true)
{
onAfterUpdate(Trigger.new, Trigger.old);
}
else if (Trigger.isInsert == true)
{
onAfterInsert(Trigger.new);
}
else if (Trigger.isUndelete == true)
{
onAfterUndelete(Trigger.new);
}
}
global virtual void hookInitialize() {/** override if necessary */}
global virtual void hookFinalize() {/** override if necessary */}
global virtual void hookException(Exception e)
{
System.debug(System.LoggingLevel.ERROR, 'Exception caught: ' + e);
throw e;
}
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};
testee.execute();
ATrigger.ALLOW_EXECUTION = true;
testee.execute();
testee.hookInitialize();
testee.onBefore();
testee.onAfter();
testee.onBeforeInsert(records);
testee.onAfterInsert(records);
testee.onBeforeUpdate(records, records);
testee.onAfterUpdate(records, records);
testee.onBeforeDelete(records);
testee.onAfterDelete(records);
testee.onAfterUndelete(records);
testee.hookFinalize();
try
{
testee.hookException(null);
System.assert(false, 'Expecting exception above!');
}
catch (Exception e)
{
System.debug('Expected exception caught: ' + e);
System.assert(true);
}
}
}
[/code]
Для использования достаточно пронаследоваться от ATrigger и переопределить соответствующие hook-методы, в самом триггере достаточно инстанцировать конретный класс и дёрнуть execute метод - дальше класс вызовет соответствующие hook-методы.
У ребят из financialforce в репозитории точно такой же класс вроде есть.
[quote="ilya leshchuk"]System.assert(true);[/quote]
Очень значимая проверка, не в обиду будет сказано.
System.assert(true);
Очень значимая проверка, не в обиду будет сказано.
А вот, кстати, проверка эта не такая дурацкая, как может показаться на первый взгляд. Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе. Так вот - не знаю как сейчас, а по тем временам тест, который не содержал ассертов считался неликвидным и отбраковывался чуть ли не на этапе автоматической проверки классов. А по-сути - покрывать в этом классе особо нечего, а этот ассерт обеспечивал выполнение того самого требования.
[quote="Gres"][quote="ilya leshchuk"]System.assert(true);[/quote]
Очень значимая проверка, не в обиду будет сказано.[/quote]
А вот, кстати, проверка эта не такая дурацкая, как может показаться на первый взгляд. Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе. Так вот - не знаю как сейчас, а по тем временам тест, который не содержал ассертов считался неликвидным и отбраковывался чуть ли не на этапе автоматической проверки классов. А по-сути - покрывать в этом классе особо нечего, а этот ассерт обеспечивал выполнение того самого требования.
System.assert(true);
Очень значимая проверка, не в обиду будет сказано.
А вот, кстати, проверка эта не такая дурацкая, как может показаться на первый взгляд. Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе. Так вот - не знаю как сейчас, а по тем временам тест, который не содержал ассертов считался неликвидным и отбраковывался чуть ли не на этапе автоматической проверки классов. А по-сути - покрывать в этом классе особо нечего, а этот ассерт обеспечивал выполнение того самого требования.
Ну про проверку я в курсе, не зря ж написал, что не в обиду, просто логической ответственности он не несет.
[quote="ilya leshchuk"][quote="Gres"][quote="ilya leshchuk"]System.assert(true);[/quote]
Очень значимая проверка, не в обиду будет сказано.[/quote]
А вот, кстати, проверка эта не такая дурацкая, как может показаться на первый взгляд. Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе. Так вот - не знаю как сейчас, а по тем временам тест, который не содержал ассертов считался неликвидным и отбраковывался чуть ли не на этапе автоматической проверки классов. А по-сути - покрывать в этом классе особо нечего, а этот ассерт обеспечивал выполнение того самого требования.[/quote]
Ну про проверку я в курсе, не зря ж написал, что не в обиду, просто логической ответственности он не несет.
Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе
Вот будет смешно, если они научатся оптимизировать и такие ассерты вообще удалятся при деплое (или не будут восприниматься). Ну и главный вопрос, что этот тест делает кроме покрытия?
[quote="ilya leshchuk"]Я упоминал, что в своё время этот класс входил в managed package, более того, если я правильно помню он входил минимум в один из managed package'й которые проходили security review на форсе[/quote]
Вот будет смешно, если они научатся оптимизировать и такие ассерты вообще удалятся при деплое (или не будут восприниматься).
Ну и главный вопрос, что этот тест делает кроме покрытия?
global abstract class ATrigger
интересно, интересно.
вот в этой части непонятно:
protected void onBeforeInsert(List<SObject> records) { hookBeforeInsert(records); for (SObject record : records) { hookBeforeInsert(record); } }
hookBeforeInsert(records); - это подготовительный период перед перебором записей и операций с ними. На этой стадии мы должны одним махом выкверить всю инфу требуюмую в цикле ниже.
Но в этом методе hookBeforeInsert(records) мы можем объявить только локальные переменные, которые уже не будет доступны вот здесь: hookBeforeInsert(record)...
[quote="ilya leshchuk"]global abstract class ATrigger[/quote]
интересно, интересно.
вот в этой части непонятно:
[quote="ilya leshchuk"] protected void onBeforeInsert(List<SObject> records)
{
hookBeforeInsert(records);
for (SObject record : records)
{
hookBeforeInsert(record);
}
}[/quote]
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 или еще что-то ... }
[quote="Den Brown"]Но в этом методе hookBeforeInsert(records) мы можем объявить только локальные переменные, которые уже не будет доступны вот здесь: hookBeforeInsert(record)...[/quote]
С чего бы это?
[code]public class ObjectATriggerRouter implements ATrigger {
private ObjectB[] objectsB;
...
public void override hookBeforeInsert(SObject[] records) {
// заполняем objectsB или еще что-то
...
}
public void override hookBeforeInsert(SObject record) {
// используем objectsB
...
}
}[/code]
global abstract class ATrigger
интересно, интересно.
вот в этой части непонятно:
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(); }
[quote="Den Brown"][quote="ilya leshchuk"]global abstract class ATrigger[/quote]
интересно, интересно.
вот в этой части непонятно:
[quote="ilya leshchuk"] protected void onBeforeInsert(List<SObject> records)
{
hookBeforeInsert(records);
for (SObject record : records)
{
hookBeforeInsert(record);
}
}[/quote]
hookBeforeInsert(records); - это подготовительный период перед перебором записей и операций с ними. На этой стадии мы должны одним махом выкверить всю инфу требуюмую в цикле ниже.
Но в этом методе hookBeforeInsert(records) мы можем объявить только локальные переменные, которые уже не будет доступны вот здесь: hookBeforeInsert(record)...[/quote]
Не совсем, можете запомнить эти данные в переменной класса, который вы создали для переопределения ATrigger, тогда они будут доступны и в hookBeforeInsert(record) методе, например:
[code]
public with sharing class ConcreteTrigger extends ATrigger {
private Object someVariable;
public override void hookBeforeInsert(List<SObject> records) {
someVariable = System.now();
}
public override void hookBeforeInsert(SObject record) {
System.assert(someVariable != null);
}
}
[/code]
Да, вот только так, через классную переменную.
остается только решить и согласовать с коллегами по цеху когда и какую стратегию выбрать: (1) делать простой тригер "все включено". (2) или тригер-разводчик с логикой выведенной в сервис методы. (3) или пустой тригер, где все выведено в такой класс как выше.
Да, вот только так, через классную переменную.
остается только решить и согласовать с коллегами по цеху когда и какую стратегию выбрать:
(1) делать простой тригер "все включено".
(2) или тригер-разводчик с логикой выведенной в сервис методы.
(3) или пустой тригер, где все выведено в такой класс как выше.
Да, вот только так, через классную переменную.
остается только решить и согласовать с коллегами по цеху когда и какую стратегию выбрать: (1) делать простой тригер "все включено". (2) или тригер-разводчик с логикой выведенной в сервис методы. (3) или пустой тригер, где все выведено в такой класс как выше.
Лично мой совет: Вариант 1 - не вариант: слишком тяжело контролировать порядок выполнения множества триггеров, если их много и слишком большой и сложный распухший триггер, если он будет включать в себя всех и вся; неудобство и сложность тестирования непосредственно триггеров.
Вариант 2, по-большому счёту, отличается от варианта 3 тем, что в варианте 3 получается 100% покрытие триггера тестами без каких-либо особых усилий, плюс гораздо легче добавить возможность вкл/выкл определенной логики, например через custom settings - в случае с триггером это сразу приведёт к падению покрытия тестами либо триггера, либо кода.
[quote="Den Brown"]Да, вот только так, через классную переменную.
остается только решить и согласовать с коллегами по цеху когда и какую стратегию выбрать:
(1) делать простой тригер "все включено".
(2) или тригер-разводчик с логикой выведенной в сервис методы.
(3) или пустой тригер, где все выведено в такой класс как выше.[/quote]
Лично мой совет:
Вариант 1 - не вариант: слишком тяжело контролировать порядок выполнения множества триггеров, если их много и слишком большой и сложный распухший триггер, если он будет включать в себя всех и вся; неудобство и сложность тестирования непосредственно триггеров.
Вариант 2, по-большому счёту, отличается от варианта 3 тем, что в варианте 3 получается 100% покрытие триггера тестами без каких-либо особых усилий, плюс гораздо легче добавить возможность вкл/выкл определенной логики, например через custom settings - в случае с триггером это сразу приведёт к падению покрытия тестами либо триггера, либо кода.
Так что вариант 3 :-)
Так что вариант 3 :-)
страшно подумать, как этого зверя представить другим разрабам. надо самому сначала все освоить и почувствовать.
Кстати есть особые случаи, это объекты общего пользования, логика на которых может дополняться с каждым проектом присоединившимся в Прод: как Контакты и Экаунты. Какой вариант будет оптимальным для них?
[quote="ilya leshchuk"]Так что вариант 3 :-)[/quote]
страшно подумать, как этого зверя представить другим разрабам.
надо самому сначала все освоить и почувствовать.
Кстати есть особые случаи, это объекты общего пользования, логика на которых может дополняться с каждым проектом присоединившимся в Прод: как Контакты и Экаунты. Какой вариант будет оптимальным для них?
Так что вариант 3 :-)
страшно подумать, как этого зверя представить другим разрабам. надо самому сначала все освоить и почувствовать.
Кстати есть особые случаи, это объекты общего пользования, логика на которых может дополняться с каждым проектом присоединившимся в Прод: как Контакты и Экаунты. Какой вариант будет оптимальным для них?
Повторюсь, вы в данном случае можете отвечать только за свой код.
Единственное что действительно надо запомнить - это порядок выполнения методов: 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>).
[quote="Den Brown"][quote="ilya leshchuk"]Так что вариант 3 :-)[/quote]
страшно подумать, как этого зверя представить другим разрабам.
надо самому сначала все освоить и почувствовать.
Кстати есть особые случаи, это объекты общего пользования, логика на которых может дополняться с каждым проектом присоединившимся в Прод: как Контакты и Экаунты. Какой вариант будет оптимальным для них?[/quote]
Повторюсь, вы в данном случае можете отвечать только за свой код.
Единственное что действительно надо запомнить - это порядок выполнения методов: 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:
[code]
public with sharing class OpportunityDispatcher extends ATrigger {
public override void hookBeforeUpdate(SObject record, SObject prior) {
dispatchComplex((Opportunity) record, (Opportunity) prior);
}
private void dispatchComplex(Opportunity opportunityRecord, Opportunity priorOpportunity) {
if (opportunityRecord.StageName == 'Closed Won' && priorOpportunity.StageName == 'In Progress') {
//do something
}
}
}
[/code]
при этом hookBeforeUpdate(SObject, SObject) запустится для каждого рекорда из контекста триггера. Если неразумно запускать этот метод отдельно для каждой записи - переопределить соответствующий bulk-метод, в данном случае hookBeforeUpdate(List<SObject>, List<SObject>).
взялся балкифицировать несколько тригеров. все они используют один и тот же метод у сервис класса, так что восходящее к sObject наследование использовалось на полную.
ок, балкифицировал тот метод: вся потенциально нужная инфа квериться до "главного" цикла с перебором "пришедших" записей (по возвожности используются такие "АПИ" методы как Account.....get('Sample').getRecordTypeId();) и подается в цикл по возможности в виде Мапов, и ДМЛ операции (если они есть в тригере) выводятся после главного цикла.
готово. но логика то относительно сложная: в зависимости от содержания трех полей на записи логика разная. как проверить что мой мэйджик метод работает как надо? подключить его к живому сендбоксу и покликать по интерфейсу?
тут то я начал понимать, что значит словочетание "unit testing"
написал код, который тестит работу моего метода по 6 потенциальным кейсам, и проверяет на выходе все измененные поля. 30 систем асертов в нем - но я еще не подключил мой метод никуда - но совершенно точно знаю, что то что он должен делать - он делает.
затем осталось только подключить метод в тригер и написать тест для тригера в котором я испытываю тригер на устойчивось к батч-операциям. Здесь уже логика не проверятся. Но покрывается тригер и определяется устойчивость к батчам на уровне самого тригера, не на уровне метода, думаю так лучше, так как это более приближенно к реальности.
Ну что ж, осталось только приодеть мои тригеры в тот костюмчик от financialforce о котором говорили выше. но это уже будет другая история
взялся балкифицировать несколько тригеров.
все они используют один и тот же метод у сервис класса, так что восходящее к sObject наследование использовалось на полную.
ок, балкифицировал тот метод: вся потенциально нужная инфа квериться до "главного" цикла с перебором "пришедших" записей (по возвожности используются такие "АПИ" методы как Account.....get('Sample').getRecordTypeId();) и подается в цикл по возможности в виде Мапов, и ДМЛ операции (если они есть в тригере) выводятся после главного цикла.
готово. но логика то относительно сложная: в зависимости от содержания трех полей на записи логика разная.
как проверить что мой мэйджик метод работает как надо? подключить его к живому сендбоксу и покликать по интерфейсу?
тут то я начал понимать, что значит словочетание "unit testing"
написал код, который тестит работу моего метода по 6 потенциальным кейсам, и проверяет на выходе все измененные поля. 30 систем асертов в нем - но я еще не подключил мой метод никуда - но совершенно точно знаю, что то что он должен делать - он делает.
затем осталось только подключить метод в тригер и написать тест для тригера в котором я испытываю тригер на устойчивось к батч-операциям. Здесь уже логика не проверятся. Но покрывается тригер и определяется устойчивость к батчам на уровне самого тригера, не на уровне метода, думаю так лучше, так как это более приближенно к реальности.
Ну что ж, осталось только приодеть мои тригеры в тот костюмчик от financialforce о котором говорили выше. но это уже будет другая история
У ребят из financialforce в репозитории точно такой же класс вроде есть.
[quote="Gres"]У ребят из financialforce в репозитории точно такой же класс вроде есть.[/quote]
я не нашел.
вот их пример тригера
[url=https://github.com/financialforcedev/df12-apex-enterprise-patterns/blob/master/df12/src/triggers/OpportunitiesTrigger.trigger]тригер[/url]
и вот их "рабочие классы" используемые в тригере:
https://github.com/financialforcedev/df12-apex-enterprise-patterns/blob/master/df12/src/classes/SObjectDomain.cls
https://github.com/financialforcedev/df12-apex-enterprise-patterns/blob/master/df12/src/classes/Opportunities.cls
SObjectDomain.cls - он же просто чудовищно сложен.
ATrigger, описанный выше, кажется более удобным для начала...
ATrigger, описанный выше, кажется более удобным для начала...
Лично я вообще не вижу смысла в классах типа ATrigger, у меня вся логика описана в сервисах, а в триггере просто вызываются нужные методы из сервиса. ATrigger нужен тем, кто не любить выносить логику в сервисы.
[quote="Den Brown"]ATrigger, описанный выше, кажется более удобным для начала...[/quote]
Лично я вообще не вижу смысла в классах типа ATrigger, у меня вся логика описана в сервисах, а в триггере просто вызываются нужные методы из сервиса.
ATrigger нужен тем, кто не любить выносить логику в сервисы.
Лично я вообще не вижу смысла в классах типа ATrigger, у меня вся логика описана в сервисах, а в триггере просто вызываются нужные методы из сервиса. ATrigger нужен тем, кто не любить выносить логику в сервисы.
у меня тоже сейчас логика в балкафицированных методах сервис класса.
и в ATrigger я собирался их вызывать как сейчас вызываю в тригере.
так зачем ATrigger нужен? - для унификации конструкции тригера в т.ч. наличие отсечки - у меня есть надежда, что один класс наследник ATrigger я смогу использовать в тригерах на разных объектах, в которых я сейчас использую абсолютно идентичную логику, а у меня больше десяти таких объектов.
[quote="Gres"]Лично я вообще не вижу смысла в классах типа ATrigger, у меня вся логика описана в сервисах, а в триггере просто вызываются нужные методы из сервиса.
ATrigger нужен тем, кто не любить выносить логику в сервисы.[/quote]
у меня тоже сейчас логика в балкафицированных методах сервис класса.
и в ATrigger я собирался их вызывать как сейчас вызываю в тригере.
так зачем ATrigger нужен?
- для унификации конструкции тригера в т.ч. наличие отсечки
- у меня есть надежда, что один класс наследник ATrigger я смогу использовать в тригерах на разных объектах, в которых я сейчас использую абсолютно идентичную логику, а у меня больше десяти таких объектов.
- для унификации конструкции тригера в т.ч. наличие отсечки
Не ижу в этом плюсов
- у меня есть надежда, что один класс наследник ATrigger я смогу использовать в тригерах на разных объектах, в которых я сейчас использую абсолютно идентичную логику, а у меня больше десяти таких объектов.
Если логика идентична, почему бы ее не собраться в одном методе и не вызывать в триггере
[quote="Den Brown"]- для унификации конструкции тригера в т.ч. наличие отсечки [/quote]
Не ижу в этом плюсов
[quote="Den Brown"]- у меня есть надежда, что один класс наследник ATrigger я смогу использовать в тригерах на разных объектах, в которых я сейчас использую абсолютно идентичную логику, а у меня больше десяти таких объектов. [/quote]
Если логика идентична, почему бы ее не собраться в одном методе и не вызывать в триггере
Просто суть ATrigger в том, что он рулит только порядком выполнения. В таком случае сам триггер никакой ответственности не несет.
Просто суть ATrigger в том, что он рулит только порядком выполнения.
В таком случае сам триггер никакой ответственности не несет.
Если логика идентична, почему бы ее не собраться в одном методе и не вызывать в триггере
там логика идентична во всех четырех ветках: до\после, вставка\апдейт.
myConcreteATrigger.execute();
вот этот метод и мог бы быть тем самым методом.
плюс возможно я смогу ре-юзнуть myConcreteATrigger.execute() во всех тригерах где требуется эта логика.
а если тригер нужно дополнить новой, присущей только этому объекту логикой, то новая логика будет в новом mySecondConcreteATrigger который пойдет второй (или первой) строкой в требуемый тригер:
[quote="Gres"]Если логика идентична, почему бы ее не собраться в одном методе и не вызывать в триггере[/quote]
там логика идентична во всех четырех ветках: до\после, вставка\апдейт.
myConcreteATrigger.execute();
вот этот метод и мог бы быть тем самым методом.
плюс возможно я смогу ре-юзнуть myConcreteATrigger.execute() во всех тригерах где требуется эта логика.
а если тригер нужно дополнить новой, присущей только этому объекту логикой, то новая логика будет в новом mySecondConcreteATrigger который пойдет второй (или первой) строкой в требуемый тригер:
myConcreteATrigger.execute();
mySecondConcreteATrigger.execute();
но это все просто размышления
получается, что это тригеры в тригере. главное преимущество в ре-юзинге такого "тригера" в разных тригерах.
Тесты в большом Орге порой валяться по каким то неожиданным причинам.
Вот только выпал тест в котором на 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."
надо спросить, под каким юзером\профайлом двигали тот ЧС и заранили тесты...
[b]PS:[/b] что-то у меня возникло нехорошее предчувствие, что 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 несущественный - не проставляйте его явно - подхватится тот что по-умолчанию.
[quote="Den Brown"]Тесты в большом Орге порой валяться по каким то неожиданным причинам.
Вот только выпал тест в котором на data-preparation стадии вставлятся Аккаунт, которому приписывается определенный РекТайп.
При очередном запуске что-то не понравлось в РекТайпе:
"INVALID_CROSS_REFERENCE_KEY, Record Type ID: this ID value in't valid for the user."
надо спросить, под каким юзером\профайлом двигали тот ЧС и заранили тесты...
[b]PS:[/b] что-то у меня возникло нехорошее предчувствие, что data-preparation фазу теста нужно выполнять под определенным Юзером (СисАдмином), иначе после начнутся чудеса, так как приписка RT для тестовой записи - это почти обязательная часть любого теста...[/quote]
Тут ситуация в том что профилю пользователя, под которым выполняется тест, недоступен тот 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?
Что значит по неожиданных причинам? Это как-то странно звучит в сфере IT.
У всего есть своя причина.
Если тесты запускает неизвестно кто, лучше себя обезопасить и создавать в тестах [b]системного юзера[/b] под которыми потом делать [b]RunAs()[/b]
Я всегда так делаю, если у заказчика есть [b]свободная лицензия для админа[/b] (этот способ кушает одну лицензию). Был один заказчик у которого все лицензии Salesforce использовались, этот способ у него не работал. Зато, кто бы не запустил тесты, можно быть уверенным что контекст не повлияет на их исполнение.
Вопрос второй. Если и это не помогает и ошибка с RecordType возникает, то надо обратить внимание как этот рекорд тайп получается из базы. Самое прикольное что я никогда не видел чтобы у заказчика (делали до меня) это дело было реализовано правильно.
Вот как должен выглядеть запрос:
[b]SELECT Id FROM RecordType WHERE DeveloperName = 'SomeName' AND SobjectType = 'Contact' AND isActive = true[/b]
Обязательно что-то да упустят, либо вместо DeveloperName просто Name, либо SobjectType не укажут, либо про isActive забудут и получают неактивные рекорд тайпы, которые при вставке в поле записи возвращают ошибку выше.
Кстати, хороший вопрос. А я ничего не забыл в запросе RecordType выше? Как проверить что у пользователя есть доступ к данному RecordType?
Тут ситуация в том что профилю пользователя, под которым выполняется тест, недоступен тот RT который в тесте пытаются проставить. Под System Administrator запускать тест не обязательно, просто если вы тестируете ситуацию с конкретным RT - создайте условия (например System.runAs) чтобы тестовому пользователю он был доступен, а если RT несущественный - не проставляйте его явно - подхватится тот что по-умолчанию.
я так и подумал
Если тесты запускает неизвестно кто, лучше себя обезопасить и создавать в тестах системного юзера под которыми потом делать RunAs() Я всегда так делаю, если у заказчика есть свободная лицензия для админа (этот способ кушает одну лицензию)
Что?! чтобы заранить под созданным (как тест дата) СисАдмин юзером нужно иметь свободную лицензию? Не такая уж редкость когда все лицензии в Проде выбраны.
А что если какого-нибудь СисАдмин выкверить? нет,для этого нужно открыть seeAllData, не хорошо.
наверное, проще попросить их более внимательно отнестить к профайлу юзера, который запускает тесты..
[quote="ilya leshchuk"]Тут ситуация в том что профилю пользователя, под которым выполняется тест, недоступен тот RT который в тесте пытаются проставить. Под System Administrator запускать тест не обязательно, просто если вы тестируете ситуацию с конкретным RT - создайте условия (например System.runAs) чтобы тестовому пользователю он был доступен, а если RT несущественный - не проставляйте его явно - подхватится тот что по-умолчанию.[/quote]
я так и подумал
[quote="Dmitry Shnyrev"]Если тесты запускает неизвестно кто, лучше себя обезопасить и создавать в тестах системного юзера под которыми потом делать RunAs()
Я всегда так делаю, если у заказчика есть свободная лицензия для админа (этот способ кушает одну лицензию)[/quote]
Что?! чтобы заранить под созданным (как тест дата) СисАдмин юзером нужно иметь свободную лицензию?
Не такая уж редкость когда все лицензии в Проде выбраны.
А что если какого-нибудь СисАдмин выкверить? нет,для этого нужно открыть seeAllData, не хорошо.
наверное, проще попросить их более внимательно отнестить к профайлу юзера, который запускает тесты..
Вот как должен выглядеть запрос: SELECT Id FROM RecordType WHERE DeveloperName = 'SomeName' AND SobjectType = 'Contact' AND isActive = true
[quote="Dmitry Shnyrev"]Вот как должен выглядеть запрос:
SELECT Id FROM RecordType WHERE DeveloperName = 'SomeName' AND SobjectType = 'Contact' AND isActive = true [/quote]
Через рефлексию еще можно
А что если какого-нибудь СисАдмин выкверить? нет,для этого нужно открыть seeAllData, не хорошо.
То что у вас seeAllData=false, вовсе не означает что база полностью пустая - куча системной информации там есть и вполне себе доступна. За полным списком надо в доку лезть, но я на 100% уверен, что это как минимум Users, Profiles и RecordTypes.
[quote="Den Brown"]А что если какого-нибудь СисАдмин выкверить? нет,для этого нужно открыть seeAllData, не хорошо.[/quote]
То что у вас seeAllData=false, вовсе не означает что база полностью пустая - куча системной информации там есть и вполне себе доступна. За полным списком надо в доку лезть, но я на 100% уверен, что это как минимум Users, Profiles и RecordTypes.
Если тесты запускает неизвестно кто, лучше себя обезопасить и создавать в тестах системного юзера под которыми потом делать RunAs() Я всегда так делаю, если у заказчика есть свободная лицензия для админа (этот способ кушает одну лицензию). Был один заказчик у которого все лицензии Salesforce использовались, этот способ у него не работал.
Попробуйте в тесте деактивировать пользователя с соответствующим типом лицензии и только после этого вызывать System.runAs c нужным вам пользователем.
[quote="Dmitry Shnyrev"]Если тесты запускает неизвестно кто, лучше себя обезопасить и создавать в тестах системного юзера под которыми потом делать RunAs()
Я всегда так делаю, если у заказчика есть свободная лицензия для админа (этот способ кушает одну лицензию). Был один заказчик у которого все лицензии Salesforce использовались, этот способ у него не работал.[/quote]
Попробуйте в тесте деактивировать пользователя с соответствующим типом лицензии и только после этого вызывать 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: []
Что, параллельно уже не получиться выполнять тесты? И тупо через раз валится эта ошибка. Один раз все тесты запускаю - проходят, второй - валится.
Вот я и приплыл.
[quote]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: [][/quote]
Что, параллельно уже не получиться выполнять тесты?
И тупо через раз валится эта ошибка. Один раз все тесты запускаю - проходят, второй - валится.
А на другом орге нет такой проблемы. И, кстати, на этом другом орге тесты запускаются ОООЧЕНЬ медленно. Могу минут пять ждать запуска тестов. Никто с такими приколами не сталкивался?
А на другом орге нет такой проблемы. И, кстати, на этом другом орге тесты запускаются ОООЧЕНЬ медленно. Могу минут пять ждать запуска тестов.
Никто с такими приколами не сталкивался?
Отключил. Обидно, что на одном орге работает, а на другом нет.
Вот я и приплыл.
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.
[quote="Chiz"]Вот я и приплыл.
[quote]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: [][/quote]
Что, параллельно уже не получиться выполнять тесты?
И тупо через раз валится эта ошибка. Один раз все тесты запускаю - проходят, второй - валится.[/quote]
А что это за объект на котором происходит LOCK? И часом там не используется seeAllData=true или версия API ниже 24 или сколько там надо чтобы seeAllData был true по-умолчанию? Просто на вскидку не понятно почему возникает lock, если по-идее каждый тест должен выполняться в независимом окружении при seeAllData=false.
[quote="ilya leshchuk"]А что это за объект на котором происходит LOCK?[/quote]Custom Object
[quote="ilya leshchuk"]И часом там не используется seeAllData=true[/quote]Используется. Как раз в этом тесте, кот отмечен.
А что это за объект на котором происходит LOCK?
Custom Object
И часом там не используется seeAllData=true
Используется. Как раз в этом тесте, кот отмечен.
Возможно в этом и причина. Я думаю что гораздо труднее будет сохранить seeAllData=true, чтобы использовать для тестов имеющиеся данные, если это так критично, и при этом разработать механизм или отрефакторить код так чтобы не возникало блокировки, чем просто переписать тесты с использованием seeAllData=false и созданием тестовых данных.
[quote="Chiz"][quote="ilya leshchuk"]А что это за объект на котором происходит LOCK?[/quote]Custom Object
[quote="ilya leshchuk"]И часом там не используется seeAllData=true[/quote]Используется. Как раз в этом тесте, кот отмечен.[/quote]
Возможно в этом и причина. Я думаю что гораздо труднее будет сохранить seeAllData=true, чтобы использовать для тестов имеющиеся данные, если это так критично, и при этом разработать механизм или отрефакторить код так чтобы не возникало блокировки, чем просто переписать тесты с использованием seeAllData=false и созданием тестовых данных.
У меня уже несколько методов, кот видят данные. Смысла писать код, для того, что бы тесты все скопом работали, я не вижу. А прогонять некоторый функционал на большом количестве объектов надо. Придется только ждать тестов. Спасибо, я как-то на SeeAllData=true и не посмотрел.
У меня уже несколько методов, кот видят данные.
Смысла писать код, для того, что бы тесты все скопом работали, я не вижу. А прогонять некоторый функционал на большом количестве объектов надо.
Придется только ждать тестов.
Спасибо, я как-то на SeeAllData=true и не посмотрел.
Используется SeeAllData=true. Как раз в этом тесте, кот отмечен.
Ох и плохая это идея. Не знаю как вы, а я к этому отношусь крайне негативно. Тесты все-таки должны работать в сферическом вакууме и не зависеть не от пользователя, который запускает тесты, ни от реальных данных. Ладно, это еще можно допустить при разработке отдельного решения для заказчика, но если пакет, то однозначно нет.
[quote="Chiz"]Используется SeeAllData=true. Как раз в этом тесте, кот отмечен.[/quote]
Ох и плохая это идея. Не знаю как вы, а я к этому отношусь крайне негативно. Тесты все-таки должны работать в сферическом вакууме и не зависеть не от пользователя, который запускает тесты, ни от реальных данных. Ладно, это еще можно допустить при разработке отдельного решения для заказчика, но если пакет, то однозначно нет.
Используется SeeAllData=true. Как раз в этом тесте, кот отмечен.
Ох и плохая это идея. Не знаю как вы, а я к этому отношусь крайне негативно. Тесты все-таки должны работать в сферическом вакууме и не зависеть не от пользователя, который запускает тесты, ни от реальных данных. Ладно, это еще можно допустить при разработке отдельного решения для заказчика, но если пакет, то однозначно нет.
Я этого тоже не люблю. Но после того, как пару раз вылезла проблемы на проде, я пишу дополнительно тесты с живыми данными. Я не могу сгенерировать достаточное количество данных в тесте. Так что приходится так.
[quote="Dmitry Shnyrev"][quote="Chiz"]Используется SeeAllData=true. Как раз в этом тесте, кот отмечен.[/quote]
Ох и плохая это идея. Не знаю как вы, а я к этому отношусь крайне негативно. Тесты все-таки должны работать в сферическом вакууме и не зависеть не от пользователя, который запускает тесты, ни от реальных данных. Ладно, это еще можно допустить при разработке отдельного решения для заказчика, но если пакет, то однозначно нет.[/quote]
Я этого тоже не люблю. Но после того, как пару раз вылезла проблемы на проде, я пишу дополнительно тесты с живыми данными. Я не могу сгенерировать достаточное количество данных в тесте. Так что приходится так.
Я не могу сгенерировать достаточное количество данных в тесте.
[quote="Gres"][quote="Chiz"]SOQL Limits[/quote]
Зачем тебе делать 100500 запросов?[/quote]В тригерах делается. Создал один тип объекта - триггер. Второй, третий, 4й, все. Птм пишется Test.startTest() и досоздается еще два типа объектов.
В тригерах делается. Создал один тип объекта - триггер. Второй, третий, 4й, все. Птм пишется Test.startTest() и досоздается еще два типа объектов.
[quote="Chiz"]В тригерах делается. Создал один тип объекта - триггер. Второй, третий, 4й, все. Птм пишется Test.startTest() и досоздается еще два типа объектов.[/quote]
Почему тогда в проде все не падает полимитам?
В тригерах делается. Создал один тип объекта - триггер. Второй, третий, 4й, все. Птм пишется Test.startTest() и досоздается еще два типа объектов.
Почему тогда в проде все не падает полимитам?
Падает. Вот и пришлось писать тесты, чтоб выявлять код, который резко увеличивает количество SOQL запросов.
[quote="Gres"][quote="Chiz"]В тригерах делается. Создал один тип объекта - триггер. Второй, третий, 4й, все. Птм пишется Test.startTest() и досоздается еще два типа объектов.[/quote]
Почему тогда в проде все не падает полимитам?[/quote]Падает. Вот и пришлось писать тесты, чтоб выявлять код, который резко увеличивает количество SOQL запросов.
Не, на проде другая проблема - 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 проблему.
[quote="Chiz"]Не, на проде другая проблема - Total number of records retrieved by SOQL queries. На дэв я создать не могу много из-за SOQL Limits, чтоб протестить Total number of records retrieved by SOQL queries проблему.[/quote]
Одна проблема рождает другую)
Не, на проде другая проблема - Total number of records retrieved by SOQL queries. На дэв я создать не могу много из-за SOQL Limits, чтоб протестить Total number of records retrieved by SOQL queries проблему.
Тестирование, на сколько я понял, начинается с того что "Допустим у нас очень много данных!" Поступите другим способом - добавьте в ваши триггера возможность их отключения (например через Custom Settings) и в тесте, когда заполняете базу данными просто их отключите. Это позволит вам хотя бы не упереться в SOQL Queries лимит.
[quote="Chiz"]Не, на проде другая проблема - Total number of records retrieved by SOQL queries. На дэв я создать не могу много из-за SOQL Limits, чтоб протестить Total number of records retrieved by SOQL queries проблему.[/quote]
Тестирование, на сколько я понял, начинается с того что "Допустим у нас очень много данных!"
Поступите другим способом - добавьте в ваши триггера возможность их отключения (например через 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. Я не пишу юнит тесты (хотя, видимо, пора начинать), я пишу функциональные тесты. Данные генерирую для каждого теста так, чтобы протестить все возможные случаи.
[quote="ilya leshchuk"][quote="Chiz"]Не, на проде другая проблема - Total number of records retrieved by SOQL queries. На дэв я создать не могу много из-за SOQL Limits, чтоб протестить Total number of records retrieved by SOQL queries проблему.[/quote]
Тестирование, на сколько я понял, начинается с того что "Допустим у нас очень много данных!"
Поступите другим способом - добавьте в ваши триггера возможность их отключения (например через Custom Settings) и в тесте, когда заполняете базу данными просто их отключите. Это позволит вам хотя бы не упереться в SOQL Queries лимит.[/quote]Мне нельзя отключать. Триггер обновляет зависимые объекты. Вычитываются другие объекты, обрабатываются и снова вставка/обновление. Без этой обработки невозможно протестить функциональность. Единственное, что у меня есть, так это флаги firstRun.
Я не пишу юнит тесты (хотя, видимо, пора начинать), я пишу функциональные тесты. Данные генерирую для каждого теста так, чтобы протестить все возможные случаи.
Я думаю ilya leshchuk имел в виду что перед тем как запускать тесты нужны тестовые данные, которые необходимо создать, а создать тебе мешают лимиты из-за навешенных на объектах триггерах. Так вот чтобы просто создать тестовые данные без проблем с лимитами можно всю функциональность нафиг отключить, потом включить и уже тесты гонять. Это чтобы не делать SeeAllData=true.
Я думаю ilya leshchuk имел в виду что перед тем как запускать тесты нужны тестовые данные, которые необходимо создать, а создать тебе мешают лимиты из-за навешенных на объектах триггерах. Так вот чтобы просто создать тестовые данные без проблем с лимитами можно всю функциональность нафиг отключить, потом включить и уже тесты гонять. Это чтобы не делать SeeAllData=true.
Мне нельзя отключать. Триггер обновляет зависимые объекты. Вычитываются другие объекты, обрабатываются и снова вставка/обновление. Без этой обработки невозможно протестить функциональность. Единственное, что у меня есть, так это флаги firstRun. Я не пишу юнит тесты (хотя, видимо, пора начинать), я пишу функциональные тесты. Данные генерирую для каждого теста так, чтобы протестить все возможные случаи.
Может я неправильно выразился, имел в виду: отключить триггера, сгенерить кучу данных, включить их обратно, протестировать. Правда при отключенных триггерах может быть проблема с той самой генерацией данных.
[quote="Chiz"]Мне нельзя отключать. Триггер обновляет зависимые объекты. Вычитываются другие объекты, обрабатываются и снова вставка/обновление. Без этой обработки невозможно протестить функциональность. Единственное, что у меня есть, так это флаги firstRun. Я не пишу юнит тесты (хотя, видимо, пора начинать), я пишу функциональные тесты. Данные генерирую для каждого теста так, чтобы протестить все возможные случаи.[/quote]
Может я неправильно выразился, имел в виду: отключить триггера, сгенерить кучу данных, включить их обратно, протестировать. Правда при отключенных триггерах может быть проблема с той самой генерацией данных.
Триггер обновляет зависимые объекты.
Почему бы не использовать класс для рекурсивного создания данных на основе каких-то принципов?
[quote="Chiz"]Триггер обновляет зависимые объекты. [/quote]
Почему бы не использовать класс для рекурсивного создания данных на основе каких-то принципов?
Они некорректно сгенерируются. Хотя, вот я тут подумал, о полном отключении всего и вся и создании тучи объектов из JSON.
[quote="Chiz"]Они некорректно сгенерируются. [/quote]
Почему ты так уверен ? У меня в проектах вроде правильно создаются.
Триггер обновляет зависимые объекты.
Почему бы не использовать класс для рекурсивного создания данных на основе каких-то принципов?
Что ты имеешь в виду под "рекурсивное создание"? Я создаю сначало List<obj1> list01. Птм создаю List<obj2> list02. Каждому obj2 в цикле назначается разный obj1. После инсерта list02 просчитываются поля на obj1 и obj2. Ну а когда дело доходит до obj5, то там пара списков вычитывается и пара списков обновляется.
[quote="wilder"][quote="Chiz"]Триггер обновляет зависимые объекты. [/quote]
Почему бы не использовать класс для рекурсивного создания данных на основе каких-то принципов?[/quote]
Что ты имеешь в виду под "рекурсивное создание"?
Я создаю сначало List<obj1> list01.
Птм создаю List<obj2> list02. Каждому obj2 в цикле назначается разный obj1. После инсерта list02 просчитываются поля на obj1 и obj2.
Ну а когда дело доходит до obj5, то там пара списков вычитывается и пара списков обновляется.
Почему ты так уверен ?
Ну например, в зависимости от поля Статус на obj2 выставляется Статус на obj1. И надо вычитать все obj2 для каждого obj1, что правильно посчитать Статус. Если тригер выключить, то статус не посчитается и я не смогу протестить функционал.
[quote="wilder"]Почему ты так уверен ?[/quote]
Ну например, в зависимости от поля Статус на obj2 выставляется Статус на obj1. И надо вычитать все obj2 для каждого obj1, что правильно посчитать Статус. Если тригер выключить, то статус не посчитается и я не смогу протестить функционал.
Хм... Может вы имеете в виду, создавать объекты с правильно заполненными полями? Логика где-то поменяется и переписыать генерацию данных.
[quote="Chiz"]Хотя, вот я тут подумал, о полном отключении всего и вся и создании тучи объектов из JSON.[/quote]И не работает, если меняется логика или добавляется/убирается поле в объекте.
Хм... Может вы имеете в виду, создавать объекты с правильно заполненными полями? Логика где-то поменяется и переписыать генерацию данных.
Так точно. Только мой класс умеет создавать данные динамически и рекурсивно. Причем данные могуть быть как случайные так и те что сам задашь.
[quote="Chiz"]Хм...
Может вы имеете в виду, создавать объекты с правильно заполненными полями? Логика где-то поменяется и переписыать генерацию данных.[/quote]
Так точно. Только мой класс умеет создавать данные динамически и рекурсивно. Причем данные могуть быть как случайные так и те что сам задашь.
сижу, пробую запилить свой первый тригер на абстрактном каркасе ATrigger. по-ходу всплывают разные вопросы:
- зачем ALLOW_EXECUTION изначально в false, и когда его лучше переводить в Тру?
global abstract class ATrigger { private static Boolean ALLOW_EXECUTION = false;
получается что этот выключатель, будучи на родительском классе, сработает одинаково для всех "триггеров" (унаследованных от него) в рантайме?
- не понял как лучше обработать исключения. Вот это есть, но как использовать еще не додумался:
например, мой класс делает ДМЛ и отправляет письма, если исключение произошло с ДМЛ - то нужно сделать красивое сообщение на записи, а если с письмами - то пусть падает как есть.
получается что это нужно организовывать в самом тригере: ловить траем, определять что за исключение и т.д. выглядит как-то неправильно...
сижу, пробую запилить свой первый тригер на абстрактном каркасе ATrigger.
по-ходу всплывают разные вопросы:
- зачем ALLOW_EXECUTION изначально в false, и когда его лучше переводить в Тру?
[quote="ilya leshchuk"]global abstract class ATrigger
{
private static Boolean ALLOW_EXECUTION = false; [/quote]
получается что этот выключатель, будучи на родительском классе, сработает одинаково для всех "триггеров" (унаследованных от него) в рантайме?
- не понял как лучше обработать исключения. Вот это есть, но как использовать еще не додумался:
[quote="ilya leshchuk"]global virtual void hookException(Exception e)
{
System.debug(System.LoggingLevel.ERROR, 'Exception caught: ' + e);
throw e;
}[/quote]
например, мой класс делает ДМЛ и отправляет письма, если исключение произошло с ДМЛ - то нужно сделать красивое сообщение на записи, а если с письмами - то пусть падает как есть.
получается что это нужно организовывать в самом тригере: ловить траем, определять что за исключение и т.д.
выглядит как-то неправильно...
- зачем 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 } }
[quote="Den Brown"]- зачем ALLOW_EXECUTION изначально в false, и когда его лучше переводить в Тру?[/quote]
ATrigger не будет работать не будучи запущенным в контексте триггера, а эта переменная добавлена для возможности обойти этот запрет в UT разрешив запуск вне контекста триггера.
[quote="Den Brown"]например, мой класс делает ДМЛ и отправляет письма, если исключение произошло с ДМЛ - то нужно сделать красивое сообщение на записи, а если с письмами - то пусть падает как есть.
получается что это нужно организовывать в самом тригере: ловить траем, определять что за исключение и т.д.
выглядит как-то неправильно...[/quote]
Зачем? Переопределяете метод hookException например так:
[code]
public override void hookException(Exception e)
{
if (e instanceof ExceptionDML)
{
//do something
}
else if (e instanceof ExceptionB)
{
//do something else...
}
else
{
//fallback
}
}
[/code]
Пока писал предыдущий пост, пришла в голову идея: иметь специальный класс exception'ов, из которых можно восстановится, соответствующий интерфейс, например
public interface IRecoverableException { Object recover(); }
...
public override void hookException(Exception e) { if (e instanceof IRecoverableException) { ((IRecoverableException) e).recover(); } //other code here }
Пока писал предыдущий пост, пришла в голову идея:
иметь специальный класс exception'ов, из которых можно восстановится, соответствующий интерфейс, например [code]
public interface IRecoverableException
{
Object recover();
}
...
public override void hookException(Exception e)
{
if (e instanceof IRecoverableException)
{
((IRecoverableException) e).recover();
}
//other code here
}
[/code]
ATrigger не будет работать не будучи запущенным в контексте триггера, а эта переменная добавлена для возможности обойти этот запрет в UT разрешив запуск вне контекста триггера.
Точно, не разглядел, что в проверке стоит ИЛИ:
if (Trigger.isExecuting == true || ALLOW_EXECUTION == true)
Зачем? Переопределяете метод hookException например так:
да, не могу привыкнуть к тому, что в новом классе переменная Trigger со всеми причендалами вроде Trigger.new[0].addError() будет доступна
[quote="ilya leshchuk"]ATrigger не будет работать не будучи запущенным в контексте триггера, а эта переменная добавлена для возможности обойти этот запрет в UT разрешив запуск вне контекста триггера.[/quote]
Точно, не разглядел, что в проверке стоит ИЛИ:
[quote="ilya leshchuk"] if (Trigger.isExecuting == true || ALLOW_EXECUTION == true)[/quote]
[quote="ilya leshchuk"]Зачем? Переопределяете метод hookException например так: [/quote]
да, не могу привыкнуть к тому, что в новом классе переменная Trigger со всеми причендалами вроде Trigger.new[0].addError() будет доступна
у меня не много опыта работы в Проде, поэтому еще есть много "наивных" вопросов по тестированию в Проде.
вот они установили managed package в Прод. Как я понимаю, покрытие кода из этого пакета также складывается в общее покрытие Орга, как и наш собственный код?
Спрашиваю, так как один проект уставил в общий Прод managed package, в котором упало значительное кол-во тестов. Всем по-барабану, так как вроде приложение работает. Но тут выяснилось, что общее покрытие упало до 74% и не возможно задвинуть новые пакеты. Тут начались волнения, что мол делать, как дальше жить...
Как можно иметь в Проде приложение у которого попадало половина тестов? ведь это не только влияние на общее покрытие, а признак того, что часть функционала приложения просто не работает...
у меня не много опыта работы в Проде, поэтому еще есть много "наивных" вопросов по тестированию в Проде.
вот они установили managed package в Прод. Как я понимаю, покрытие кода из этого пакета также складывается в общее покрытие Орга, как и наш собственный код?
Спрашиваю, так как один проект уставил в общий Прод managed package, в котором упало значительное кол-во тестов. Всем по-барабану, так как вроде приложение работает. Но тут выяснилось, что общее покрытие упало до 74% и не возможно задвинуть новые пакеты. Тут начались волнения, что мол делать, как дальше жить...
Как можно иметь в Проде приложение у которого попадало половина тестов? ведь это не только влияние на общее покрытие, а признак того, что часть функционала приложения просто не работает...
Спрашиваю, так как один проект уставил в общий Прод managed package, в котором упало значительное кол-во тестов. Всем по-барабану, так как вроде приложение работает.
Это очень плохо если нет человека который это понимает, программиста который за это отвечает. Сам пару раз сталкивался с такими проектами - нанимали человека когда-то сделать кусок функционала - тот делал не очень качественно и умывал руки. Со временем тесты на проде начали валиться, но всем пофиг, потому что "вроде" работает.
Мне очень жаль такие проекты - они потихоньку умирают. Над каждым кодом должен кто-то стоять, за каждым кодом должен кто-то следить. Пусть и не постоянно, на основе "поддержки", но это должно быть.
Короче, по твоему вопросу, Den. Конечно надо разбираться почему упали тесты и чинить их. Тут ничего не поделаешь. И если заказчик не хочет платить за то что ты будешь чинить эти косяки, то пусть ищут тех кто виновен и трясти их.
[quote="Den Brown"]Спрашиваю, так как один проект уставил в общий Прод managed package, в котором упало значительное кол-во тестов. [b]Всем по-барабану, так как вроде приложение работает[/b].[/quote]
Это очень плохо если нет человека который это понимает, программиста который за это отвечает.
Сам пару раз сталкивался с такими проектами - нанимали человека когда-то сделать кусок функционала - тот делал не очень качественно и умывал руки. Со временем тесты на проде начали валиться, но всем пофиг, потому что "вроде" работает.
Мне очень жаль такие проекты - они потихоньку умирают. Над каждым кодом должен кто-то стоять, за каждым кодом должен кто-то следить. Пусть и не постоянно, на основе "поддержки", но это должно быть.
Короче, по твоему вопросу, Den. Конечно надо разбираться почему упали тесты и чинить их. Тут ничего не поделаешь. И если заказчик не хочет платить за то что ты будешь чинить эти косяки, то пусть ищут тех кто виновен и трясти их.
вот они установили managed package в Прод. Как я понимаю, покрытие кода из этого пакета также складывается в общее покрытие Орга, как и наш собственный код?
Нет, покрытие кода из managed package не влияет на общее покрытие кода в организации.
Спрашиваю, так как один проект уставил в общий Прод managed package, в котором упало значительное кол-во тестов. Всем по-барабану, так как вроде приложение работает. Но тут выяснилось, что общее покрытие упало до 74% и не возможно задвинуть новые пакеты. Тут начались волнения, что мол делать, как дальше жить...
Само покрытие managed package на среднее покрытие кода в организации не влияет, но вот package может влиять опосредованно, а именно завалив существующие тесты. Например какой-нибудь Country Complete managed package добавляет валидацию на поля "country", на соответствие их какому-нибудь ISO стандарту, да ещё и по-умолчанию навешивается на парочку стандартных объектов - Contact, Account и т.д. С большой долей вероятности эти стандартные объекты используются в существующих тестах и теперь эти тесты могут нечаянно перестать работать.
[quote="Den Brown"]вот они установили managed package в Прод. Как я понимаю, покрытие кода из этого пакета также складывается в общее покрытие Орга, как и наш собственный код?[/quote]
Нет, покрытие кода из managed package не влияет на общее покрытие кода в организации.
[quote="Den Brown"]Спрашиваю, так как один проект уставил в общий Прод managed package, в котором упало значительное кол-во тестов. Всем по-барабану, так как вроде приложение работает. Но тут выяснилось, что общее покрытие упало до 74% и не возможно задвинуть новые пакеты. Тут начались волнения, что мол делать, как дальше жить...[/quote]
Само покрытие managed package на среднее покрытие кода в организации не влияет, но вот package может влиять опосредованно, а именно завалив существующие тесты. Например какой-нибудь Country Complete managed package добавляет валидацию на поля "country", на соответствие их какому-нибудь ISO стандарту, да ещё и по-умолчанию навешивается на парочку стандартных объектов - Contact, Account и т.д. С большой долей вероятности эти стандартные объекты используются в существующих тестах и теперь эти тесты могут нечаянно перестать работать.
Нет, покрытие кода из managed package не влияет на общее покрытие кода в организации.
ну вот, уже легче, значит нужно подправить только собственные тесты. надо бы проверить, влияет ли на них тот пакетдж, но не думаю, что там могут быть какие то грубые косяки с общиими объектами.
[quote="ilya leshchuk"]Нет, покрытие кода из managed package не влияет на общее покрытие кода в организации.[/quote]
ну вот, уже легче, значит нужно подправить только собственные тесты. надо бы проверить, влияет ли на них тот пакетдж, но не думаю, что там могут быть какие то грубые косяки с общиими объектами.
ну вот, уже легче, значит нужно подправить только собственные тесты.
Тоже не 100% вариант. Возможно твой функционал влияют на тесты внутри managed package. Например это у тебя есть какое нибудь required поле на стандартном объекте, которое не дает тестам из пакета вставить запись, потому что пакет не знает об этом поле.
Этот вопрос уже я поднимал - почему плохо задействовать стандартные объекты для managed package.
[quote="Den Brown"]ну вот, уже легче, значит нужно подправить только собственные тесты.[/quote]
Тоже не 100% вариант. Возможно твой функционал влияют на тесты внутри managed package. Например это у тебя есть какое нибудь required поле на стандартном объекте, которое не дает тестам из пакета вставить запись, потому что пакет не знает об этом поле.
Этот вопрос уже я поднимал - почему плохо задействовать стандартные объекты для managed package.
Этот вопрос уже я поднимал - почему плохо задействовать стандартные объекты для managed package.
В моем пакете класс, который генерит дату, делает это динамически. Это правда не спасет если вы повесите еще и валидейшен рул на это поле.
[quote="Dmitry Shnyrev"]Этот вопрос уже я поднимал - почему плохо задействовать стандартные объекты для managed package.[/quote]
В моем пакете класс, который генерит дату, делает это динамически. Это правда не спасет если вы повесите еще и валидейшен рул на это поле.
Это правда не спасет если вы повесите еще и валидейшен рул на это поле.
Или триггер свой придумаю хитрый на стандартном объекте
[quote="wilder"]Это правда не спасет если вы повесите еще и валидейшен рул на это поле.[/quote]
Или триггер свой придумаю хитрый на стандартном объекте ;)
Или триггер свой придумаю хитрый на стандартном объекте
Но при желании можно придумать какой-то стандартный интерфейс для общения пакетов и создания тестовых данных.
[quote="Dmitry Shnyrev"]Или триггер свой придумаю хитрый на стандартном объекте[/quote]
Но при желании можно придумать какой-то стандартный интерфейс для общения пакетов и создания тестовых данных.
Что-то не совсем понятно. Кто будет придумывать и кто будет использовать. Если свои пакета, может быть. А если ты ставишь пакеты от какого нибудь КогаддуБираппаТимаппаНаира, как ты это дело собираешься под твою концепцию подстраивать?
Что-то не совсем понятно. Кто будет придумывать и кто будет использовать.
Если свои пакета, может быть. А если ты ставишь пакеты от какого нибудь КогаддуБираппаТимаппаНаира, как ты это дело собираешься под твою концепцию подстраивать?
Что-то не совсем понятно. Кто будет придумывать и кто будет использовать. Если свои пакета, может быть. А если ты ставишь пакеты от какого нибудь КогаддуБираппаТимаппаНаира, как ты это дело собираешься под твою концепцию подстраивать?
Нужно разработать открытый интерфейс и его должны все использовать :)
[quote="Dmitry Shnyrev"]Что-то не совсем понятно. Кто будет придумывать и кто будет использовать.
Если свои пакета, может быть. А если ты ставишь пакеты от какого нибудь КогаддуБираппаТимаппаНаира, как ты это дело собираешься под твою концепцию подстраивать?[/quote]
Нужно разработать открытый интерфейс и его должны все использовать :)
его должны все использовать :)
Если бы ты не говорил что собираешься заняться своим делом, я бы подумал что в ближайшее время ты собираешься занять один из руководящих постов в департаменте разработки Salesforce.
[quote="wilder"]его должны все использовать :)[/quote]
Если бы ты не говорил что собираешься заняться своим делом, я бы подумал что в ближайшее время ты собираешься занять один из руководящих постов в департаменте разработки Salesforce. :D
[quote="Gres"]Генерация имен?[/quote]
Нет :D отсюда взял http://habrahabr.ru/post/130361/
нашел по запросу "индийское имя сломаешь язык" :D
Хотел найти имя из Камеди Клаб вот из этого ролика, но на слух не получается :D
https://www.youtube.com/watch?v=NfMVRhmdKxs (c 3.07)
:D
в департаменте разработки Salesforce
Видел как у них построены процессы и убедился, что я не хочу работать внутри.
[quote="Dmitry Shnyrev"]в департаменте разработки Salesforce[/quote]
Видел как у них построены процессы и убедился, что я не хочу работать внутри.
подскажите, люди добрые,
когда в проде калькулируется процент среднего покрытия, то как идет расчет среднего процента:
суммируются проценты покрытия каждого класс\тригера и из этой суммы вычисляется средний процент или суммируется общее кол-во строк и затем строк покрытого кода, и в конце выводится средний процент по Оргу?
подскажите, люди добрые,
когда в проде калькулируется процент среднего покрытия, то как идет расчет среднего процента:
суммируются [b]проценты[/b] покрытия каждого класс\тригера и из этой суммы вычисляется средний процент
или
суммируется общее кол-во строк и затем строк покрытого кода, и в конце выводится средний процент по Оргу?
потому что это могут быть совсем разные цифры...
суммируется общее кол-во строк и затем строк покрытого кода, и в конце выводится средний процент по Орг
суммируется общее кол-во строк и затем строк покрытого кода, и в конце выводится средний процент по Орг
столкнулся в тесте с такой ситуацией:
в контроллере создаются дочерние записи и вставляются. но у него есть метод "клонирующий" предыдущий кастомерский ввод, т.е. эти дочерние записи используются повторно, у них просто переписывается значение в Master-Detail поле.
и все работает ОК.
а в тесте не работает: это Master-Detail поле становится неРедактируемым после вставки записи. Долго думал, что за ерунда.
Оказывается, что это Master-Detail поле действительно становится неРедактируемым после вставки записи. Но в реальном контроллере вставка и перезапись происходит в разных обращениях к серверу, и поле перезаписыватся. А в тесте я пытался последовательно вызывать методы у одного и того же экземпляра Контроллера, и там такой номер не прошел. пришлось несколько раз создавать контроллер.
столкнулся в тесте с такой ситуацией:
в контроллере создаются дочерние записи и вставляются.
но у него есть метод "клонирующий" предыдущий кастомерский ввод, т.е. эти дочерние записи используются повторно, у них просто переписывается значение в Master-Detail поле.
и все работает ОК.
а в тесте не работает: это Master-Detail поле становится неРедактируемым после вставки записи. Долго думал, что за ерунда.
Оказывается, что это Master-Detail поле действительно становится неРедактируемым после вставки записи. Но в реальном контроллере вставка и перезапись происходит в разных обращениях к серверу, и поле перезаписыватся. А в тесте я пытался последовательно вызывать методы у одного и того же экземпляра Контроллера, и там такой номер не прошел. пришлось несколько раз создавать контроллер.
Интересное наблюдение.
Я так понимаю что у тебя на master-detail стоит "Allow reparenting" что оно изменяется в логике?
А можно с примером кода из контроллера и в идеале со скриншотом полей объектов. Просто, судя по написанному, такого быть не может - если поля не reparentable, то переподвязать чайлды к новому паренту не получится, максимум что происходит в контроллере: старые чайлды клонируются, им проставляются лукапы на новых парентов, новые чайлды вставляются в базу, а старые удаляются.
То что надо создавать новый экземпляр Контроллера говорит о том что вы не до конца разобрались с проблемой, это никоим образом не должно влиять на данный функционал.
[quote="Den Brown"]нет, поле не Reparentable Master Detail[/quote]
А можно с примером кода из контроллера и в идеале со скриншотом полей объектов. Просто, судя по написанному, такого быть не может - если поля не reparentable, то переподвязать чайлды к новому паренту не получится, максимум что происходит в контроллере: старые чайлды клонируются, им проставляются лукапы на новых парентов, новые чайлды вставляются в базу, а старые удаляются.
То что надо создавать новый экземпляр Контроллера говорит о том что вы не до конца разобрались с проблемой, это никоим образом не должно влиять на данный функционал.
А можно с примером кода из контроллера
Public class Question_Answer { Public Question__c question { set; get; } Public Answer__c answer { set; get; }
}
Public List<Question_Answer> QA_list { set; get; } // идет на фронт
Public Boolean insertRecords() {
Parent__c parent = new Parent__c();
List<Answer__c> ansList = new List<Answer__c>();
Savepoint sp = Database.setSavepoint();
try{
insert parent;
for(Question_Answer qa: QA_list) {
qa.answer.MyParent__c = parent.id; // присваиваю в первый раз или ПЕРЕЗАПИСЫВАЮ родителя
ansList.add(qa.answer); }
insert ansList;
return true; } catch(System.Exception e){
Database.rollback(sp);
...
return false;
}
}
Public PageReference SubmitAndClone() { Boolean isSuccess = insertRecords();