Comments 39
Метод "Паблик Морозофф" сам применял в дебаге лет 15 назад. Потом перешли на инъекцию записей friend в пайплайне сборки тестов. С тех пор считаю, что правильная реализация private должна позволять его нарушать. Кто оставил в проде ССЗБ, ворнинг кидать можно, но должно работать - для отладки, для юниттестов тоже.
Мне кажется что проблема скорее в дизайне. Я так понял что у вас есть god object нечеловеческих размеров, который делает половину работы всей программы. Соответственно, вы хотите тестировать какие-то отдельные куски работы этого объекта. В то время как правильнее было бы иметь объекты более вменяемых размеров и тестировать только их внешнее поведение, оставляя детали внутренней реализации на совести объекта. В таком случае вам бы пришлось дергать только публичные методы. Я так понимаю, это ближе к сути юнит-тестирования.
А возможно (крамольная мысль) что ООП вам вовсе и не нужен... По крайней мере в том модуле.
Примерно та же мысль. Мне всегда казалось, что больше половины пользы от юнит-тестов – в том, что вы вынуждены структурировать программу соответствующим образом.
Не только вам кажется, использование юниттестов ведет к изменению API вплоть до breaking changes и декомпозиции тестируемых классов, что конечно лучшим образом сказывается на всем проекте. Да только это не всегда можно сделать, по разным причинам. Хорош не тот продукт, в котором все идеально (идеальных вещей вообще нет), а тот который продается и позволяет разививаться дальше (с)
Чёт не понял. Я знаю JS/TS (мой хлеб с маслом) и C#. Достаточно различные языки, в которых тем не менее никакие нормальные тесты в приватную часть тестируемого класса не лезут. Всё мокается/перехватывается на подходе, до того как подменяемая зависимость навсегда "пропадёт" в приватке инстанса. А про чисто внутренний стейт я вообще молчу - это личное дело класса, которое не должно беспокоить даже его тесты. Причём в этих языках подглядеть/поменять содержимое private в рантайме ещё проще, чем в плюсах, но этого не делают.
Вы с чем-то близким к железу работали? Когда надо в тестах полазить во всякие внутренности буферов? Казалось бы не надо, но практика такова что вы без этого часто вообще невозможно понять что идёт не так и какого чёрта оно ведёт себя так, как ведёт. Потому что документация часто неполна, штатный софт крив и работает правильно при определённом стечении обстоятельств. Да, залезая вовнутрь при помощи пабликов морозовых мы огребаем все минусы этого решения. да оно может отвалиться при обновлении, да, всё что вы скажете против паблика да, но это цена которую приходится заплатить чтобы это чудище работало.
Есть особенности работы с железом, например надо перехватить буфер на каком то моменте обработки, писать для этого отдельную логику проблематично, обычно просто дампят Стейт класса и дальше уже по офсетам смотрят что где и как лежит
А если гетера нет или переменная класса спрятана в private секцию и возможность изменить хедер отсутствует.
У вас для всех примеров требуются изменения в хедерах. Но если хедеры можно изменить, то гораздо проще для тестовых сборок менять приватные и защищенные методы и свойства на публичные каким нибудь макросом, чем заморачиваться со свистопляской , описанной в статье.
Больше времени останется на продуктивную работу, чем искать и исправлять проблему там, где её можно просто обойти.
Всё ещё не понял, чем не устроил Паблик Морозов? Это же код юнит тестов, там можно любые непотребства и антипаттерны в угоду читаемости и независимости тестов друг от друга.
Конечно, лучше проектировать нормально, но когда возможности есть (код чужой/старый), то Паблик Морозов нормальное решение. Если компилятор нельзя победить с его запретами (что странно, диагностики обычно можно задавитт), то просто можно пройтись sed-ом перед компиляцией тестов.
#define private public // illegal
#define class struct // illegal
Если компилятор нельзя победить с его запретами (что странно, диагностики обычно можно задавитт), то просто можно пройтись sed-ом перед компиляцией тестов.
Можно гораздо проще (сам пользуюсь данным способом уже второй десяток лет без каких либо проблем с компиляторами):
#ifdef UNITTEST
#define SCOPE(scope) public
#else
#define SCOPE(scope) scope
#endif
class Name {
SCOPE(private):
...
};
Это модификация заголовочных файлов. С этим и sed справится, нет никого смысла городить такие костыли.
Победить не удалось на тот момент, каких то ключей диагностики я не нашел, чтобы это отключить. А ждать фикса от кейла... ну про полгода на фикс я написал уже
Мой опыт показывает, что в 999 случаев из 1000 необходимость в тестировании внутренних методов класса говорит о неправильной декомпозиции. В очень редких случаях, это действительно нужно, но эти тесты уже что-то среднее между интеграционными и юнит-тестами.
Не понял, а добавить один friend
класс и все тесты наследовать от него нельзя что ли?
https://onlinegdb.com/p5fXE6QPF
Нет, во френдах наследование прав не работает, это четко прописано в стандарте
Как я показал в примере, один friend
класс дает доступ ко всем полям, а тесты наследуются от него (или просто рядом лежат, тогда класс должен public
доступ давать). И не надо каждый тест индивидуально делать friend
-ом.
Просто если и так при создании каждого нового теста правится исходный класс, то как бы логично было сразу сделать один класс, дающий доступ, а все остальные чтобы юзали его. Минимум усилий по переписыванию, идея на поверхности и вообще непонятно, зачем что-то другое изобретать. Только в качестве упражнения.
Не про программирование, но всё же: гетеры - это несколько про другое ( https://ru.wikipedia.org/wiki/Гетера ). Может, лучше геТТеры?
Мне же не одному кажется, что если вы пишите юниты на приватные члены, то в разряд говна переходит не только ваш код, архитектура, но и сами юниттесты?
Какой смысл делать что-то приватным, если при его изменении вы сломаете тесты? Делайте частью интерфейса.
Возможно случай когда в приватных методах есть какая то логика которую стоит тестировать и альтернатива — эта логика будет просто не тестируемой?
(И при этом по каким то причинам @VisibileForTesting или его аналог для конкретного языка либо нельзя либо не срабатывает).
У нас вот сейчас такое бывает. Но Kotlin. Android. И по новым внутренним правилам — тесты вообще пишутся по возможности (=ревью можно пройти без тестов).
Если вы уже всё равно взялись за ужасные решения, извращающие саму суть ооп, почему бы в тестах не сделать копию определения тестируемого класса без ключевого слова private (например, кодогенерацией) и в тестах просто не реинтерпретировать указатель/ссылку на объект к этому типу? И вот все поля доступны без магии, и исходный код не страдает
(за такое, конечно, увольняют, но за описанное в статье вообще расстреливают)
Было уже, #define private public и работает без кодогенерации. Ну тут мы возвращаемся к вопросу вам шашечки или ехать? ПМ выбрал ехать, бороться с менеджментом?
Define private public тоже ужасно, но оно ещё и аффектит тестируемый код.
Где нет правильных шашечек, со временем перестаёт ехать. Потому что это не просто так шашечки.
Если менеджмент за программистов выбирает инженерные решения, то зачем нужны программисты?
Ок, мы выбрали переделывать архитектуру проекта, чтобы сделать красиво и правильно, отвлекли людей, зарылись в рефактор и не успели по срокам, сорвали раунд финансирования, остались без денег, люди ушли на другой проект (врядли они останутся с нами за добрые слова и обещания всех денег мира). Шашечки правильные, но увы не едет. Я как программист оцениваю техническую сторону, пм оценивает время и возможности, не думайте что они свой хлеб просто так едят. Может они и не умеют в шаблоны, зато прекрасно умеют в планирование.
Т.е. вы готовы увольнять инженеров, которые понимают тонкости языка и могут написать такое решение? Саттер с вами не согласен, некоторое время этот вопрос присутствовал на его собесах в команду архитектов маек
Вы вдвоем приводите в качестве примеров крайности, но почему то забываете, что их как правило не бывает. Точнее, если они случаются, то существуют очень не долго ровно по описанным вами причинам (до закрытия финансирования).
Но ведь в действительности, как менеджер должен принимать в расчеты несколько вариантов архитектурных решений, так и архитектор должен быть в курсе стратегии развития бизнеса, чтобы учесть это при развитии продукта.
Ваши примеры, это принятие решений на основе компромисса двух ролей. Но это очень сложный и зачастую сложный путь, который может окончится ничем (например, если один из двух упрется рогом).
//Пословица не имей сто рублей а имей сто friend более не актуальна.
//Таких friend за end в museum.
Есть вопрос по этому месту.
template <class Stub, typename Stub::type x>
struct private_member { private_member() { member<Stub>::value = x;
} // сохранение адреса переменной };
У вас в OnlineGDB здесь есть еще static private_member instance
в котором не хватает inline, т.к. не создается экземпляр private_member, мы не попадаем в конструктор и по факту
papa.*member<A_x>::value = "deneg net";
std::cout << papa.*member<A_x>::value << std::endl;
кладёт данные куда то мимо поля private. Это видно в отладке, или если попытаться сделать вывод до и после например с помощью публичного метода.
Вот вывод прям по вашему примеру без inline:
template <class Stub, typename Stub::type x>
struct private_member {
private_member() { member<Stub>::value = x;
static private_member instance;
} // сохранение адреса переменной };
// Остальной код пропущен для краткости
int main() {
PapaPavlica papa;
std::cout << papa.*member<A_x>::value << std::endl;
papa.*member<A_x>::value = "deneg net";
std::cout << papa.*member<A_x>::value << std::endl;
}
Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
А вот с inline:
template <class Stub, typename Stub::type x>
struct private_member {
private_member() { member<Stub>::value = x;
inline static private_member instance;
} ;// сохранение адреса переменной
// Остальной код пропущен для краткости
int main() {
PapaPavlica papa;
std::cout << papa.*member<A_x>::value << std::endl;
papa.*member<A_x>::value = "deneg net";
std::cout << papa.*member<A_x>::value << std::endl;
}
papini dengi
deneg net
Process finished with exit code 0
Благодарю что нашли ошибку, поправил
А про inline
можете объяснить, если не сложно? Не очень понятно как оно должно быть, но без него у меня ни в OnlineGDB ни на компьютере не работает.
тут все просто, мы просто сохраняем оффсет для приватной переменной A::x, этот оффсет и хранится в member::value, но чтобы различать A::x и B::x, нужен еще один промежуточный тип данных для хранения типа класса, чтобы потом восстановить из него нужный оффсет для конкретного класса. private_member<A>::value и будет нужный оффсет
формально код Саттера не ломает инкапсуляцию -- вы делаете специализацию одного из методов класса, очевидно, что класс имеет доступ к своим private членам
формально так, но позже в 2017 году Комитет пришел к выводу, что такая конструкция является нарушением инкапсуляции. В стандарте это явно не запрещено, но делать так не рекомендуется
Когда private, но очень хочется public