Entity в ORM — это DTO для репозитория. Зачем вам DTO юнит-тестами покрывать? Там же нет и не должно быть логики.
Даже в мануалах по тестам мокают репу и возвращают готовую энтити.
Если же вам нужно проверить, что все связи настроены правильно, то это уже интеграционное тестирование, где вы проверяете, как меняется состояние при взаимодействии репы и базы: репа правильно заполняет энтити, и правильно пишет в базу.
Как раз очень хорошо показывает, есть ли у человека понимание, что скрывается за типом данных, или он просто перегоняет одно в другое, чтобы было удобно / под требования.
В продакшен код, особенно в части взаимодействия по api, практически каждый второй пишет что-то, вроде
return (int)...
...
method((int)$property,...
Или любой другой нужный тип. Так как используемая библиотека требует данных определённого типа. Без понимания, что преобразование типов — это не бесплатная фича динамической типизации. И потеря точности — меньшее из зол. Пример выше как раз о том, что мы таким образом может передать не те данные, что планировали.
Людей, которые сходу говорили, что float (числа с плавающей запятой) — это, вообще-то, диапазон значений, вообще, единицы. Не говоря уже о том, что физический смысл математических операций над float, — это совершенно не то, что многие себе представляют.
Библиотека предназначена для парсинга, экземпляр класса живёт одну итерацию цикла, в одной ветке условного оператора
Вот я и говорю, что у вашего класса довольно нетривиальный интерфейс. Шаг влево, шаг вправо — проведение объекта непредсказуемо.
Это не библиотечный функционал, а хелпер под конкретный, причем типовой, случай.
Т.е. выбранными вами архитектурными решениями, вы неявно резко ограничили применение классов исключительно рамками одной типовой задачи. Это решение может казаться хорошим, пока не захочется переиспользовать данные классы для чего-то другого. И тогда придется решать проблему отсутствия определенных гарантий у получаемых объектов, и весь код сразу станет не таким уж и классным.
Всё это делается средствами языка. Пример уже приводили. (type) — это каст. ?? — альтернатива, если null.
null ===
gettype / is_ — для получения и проверки типов.
А криминал в том, что для случае null вы, зачем-то, прогоняете альтернативное значение через объект, делая его интерфейс крайне нетривиальным. Например, в каком состоянии будет объект после второго применения default? Можно ли так вообще делать? Вызывая метод default, я откуда-то могу знать, что это объект больше никто не будет использовать? И т.д.
За велосипед с default хочется отправить почитать что-нибудь про Option. Не говоря уже о том, что ValueHandler объединяет в себе и конструктор объекта, и трансформацию, и хранилище. Жуткий класс. Хочется отдать его трем разным людям, чтобы каждый сделал свою часть правильно.
Нормальный этап взросления.
Сначала языковая конструкция кажется ребусом.
Пишется своя реализация, удобная, читаемая.
Потом добавляется в неё сахарок… И где-то начинают крутиться мысли, а что делает мой велосипед такого, что я не использую встроенные в язык конструкции?
?? кинет ворнинг при отсутствии элемента с таким ключом — это фиаско.
Использовать тайп каст, чтобы передавать в функции значения нужных типов — это эпик фейл.
Практически любой сериалайзер сделает всё тоже самое, что и библиотека автора, только скажи в какой объект сложить данные. И к преобразованию типов отнесется со всей серьёзностью, с предсказуемыми ошибками, если данные без потерь не кастятся в целевой тип.
Чем там гордиться? Тем, что такую специализированную оптимизацию поддерживать — запредельное зло?
Это надо самому поработать с таким кодом, доставшимся в наследство. Вместо ясной-понятной логики с обработкой крайних случаев, одна сплошная мешанина, где на каждый чих — уникальное решение.
Смотришь на такое и понимаешь: "работает — не трожь". Слишком долго и дорого это менять
Когда проектом занимается человек, который знает фреймфорк, на котором пишет, то, смотря на результат его трудов, видишь, что о том, что сделано, знания шерить ненужно.
А когда читаешь говно-код мастера «вам шашечки или ехать», то сразу возникают вопросы: а что это за валидация такая интересная, что это за бест-практики тестирования такие, почему нельзя было использовать стандартный логер? И самая главная головная боль: теперь надо будет учить (тратить время) других, чтобы они разбирались в этих выдающихся инженерных решениях, не имеющих ни документации, ни известных за многолетный опыт использования сообществом знаний о преимуществах и недостатках данных конкретных решений.
Намного выгоднее, используя в проекте конкретный фреймфорк, дать время новичку освоить его практики, а не позволять тащить бекграунд его предыдущих проектов сюда.
Опытный и так себе не позволит, работая в рамках фрейморка, изобретать велосипед для того, что итак уже в нём реализовано. Даже если он раньше с этим не сталкивался. В этом его ключевое отличие от новичка — не тащить за собой свои «достижения» из компании в компанию: я на прошлом месте работы так делал!
Хоть свой пример не показателен, но имперически для себя выяснил, что переписывание материала — лучший способ его забыть. Запоминается он, только когда начинаешь с ним работать, крутишь под разными углами и отсутствие источника ответов на возникающие вопросы заставляет погружаться в материал. Для меня лучшим подспорьем от лекции — это список литературы, где изучить тему, и список вопросов, понимание которых свидетельствует, что разобрался в теме.
Вопрос в том, что вы хотите подтвердить тестом, который работает с ответом, полученным от функции encrypt? То, что в вашем проекте есть функция (например, decrypt), которая выдаст ожидаемый результат? Или то, что некая внешняя библиотека может расшифровать эти данные?
Те же вопросы и для функции decrypt? Вы хотите подтвердить, что эта функция может расшифровать ответ от вашей функции encrypt? Или расшифровать сообщение, зашифрованное какой-то сторонней библиотекой?
2. У вас функция шифрует данные (пусть алгоритмом AES).
Ваша задача убедиться, что вы правильно шифруете (правильно вызвали библиотечный код — там много подводных камней), и удаленный получатель сможет их расшифровать.
Проблема: для одной и той же входной последовательности шифр всегда разный.
Какой контракт мы хотим подтвердить своим тестом? Что клиент расшифрует данные, которые мы ему отправим? Т.е. мы проверяем то, что данные, которым мы отправим, могут быть расшифрованы.
Тест тут прост и ясен: encryptedData = encrypt(expectedData)
assertDecrypt(expectedData, encryptedData);
В assertDecrypt мы прячем реализацию decrypt, которую рассматриваем как эталонную: наш контракт таков, что зашифрованные данные будут расшифрованы данной реализацией. Используйте её или совместимую. Мы проверяем, что наша функция encrypt возвращает данные, которые могут быть расшифрованы этой эталонной реализацией.
Если мы хотим проверить, что мы можем расшифровать некие зашифрованные данные, то подход тот же: decryptedData = decrypt(encryptedData);
assertEqual(expectedData, decryptedData);
Здесь мы проверяем, что наша реализация получает правильные данные, расшифровывая некие зашифрованные данные.
Но вот чего точно нельзя делать, так это взаимно использовать decrypt <-> encrypt в рамках одного теста, так как вместо валидации контракта, мы валидируем обратимость функций, о чём тут уже упомянули.
Но проблема же именно в связанности контекстов, которые по большей части лежат в плоскости БД, или вообще в сторонних сервисах. По сути, для того чтобы нормально проверять классы, надо инвертировать зависимости и, например, отказываться от применения орм, поскольку он слабо тестируем. И естественно, выпиливать работу с бд напрямую.
Вопрос в том, какую конкретно задачу вы хотите решить тестами?
Если проверять, что методы правильно раскладывают данные в базе данных, то надо делать тестовую бд, in-memory либо ещё как-то.
Если проверять, что методы правильно подготавливают данные перед тем, как они сохраняются через орм в бд, то нужно эти методы отделить от орм в отдельный слой и тестировать их. А орм использовать как зависимость, вроде репозитория.
И т.д. Сначала, что именно хочется проверять и зачем, потом реализация архитектуры под эту задачу.
Иногда пытаюсь подступиться к этой задаче, но каждый раз ломаю зубы и откатываюсь.
Начните с простого.
Выберите какой-нибудь простой класс, с наименьшим количеством зависимостей и простой функциональностью, и напишите на его интерфейс модульные тесты.
Дальнейшее движение по более сложным классам вскроет довольно много интересного в используемой архитектуре, когда вы чуть ли не каждый метод каждого класса будете распознавать как for single use only. Т.е. из-за сильной связанности данный метод применим (полезен) только в данной конкретной ситуации, и развязать весь контекст, чтобы изолированно протестировать этот метод, не так уж и просто.
Такой вот спагети-код, когда что-либо конкретное в системе расширить довольно сложно, кроме как написать новую спагетину, пропуская её через весь код.
Практика написания юнит тестов добавляет навык видеть подобный код. Он может быть красивым и стройным, с маленькими понятными классами, но архитектурно не переиспользуемым.
Другим запахом такого кода являются мини итеграционные тесты, когда мы не можем написать на класс модульные тесты из-за связанности контекстов, и вынуждены писать тест, проверяющий цепочку вызовов.
Не совсем согласен. Иногда бывает так, что тестируется какой-то особо хитрый corner case, который возникает после какой-то определенной последовательности действий.
Для этого существуют фикстуры. Готовим нужное состояние системы под каждое действие и проверяем, что оно работает так, как задумано.
Иначе мы просто впустую тратим время, инициализируя всё необходимое для всей цепочки вызовов, чтобы нужные состояния создавались самой системой, а не были захардкожены. Это тоже test smell, когда методы тестируются не изолированно друг от друга. И ещё один способ получить флаки тесты.
При этом, когда такой связанный тест упадёт, мы сразу поймём, что его архитектура (связанность) бесполезна для нас, так как в процессе починки такого теста, его придётся разбивать на части, чтобы понять, что именно пошло не так.
Хороший модульный тест своим фейлом говорит, что «вот этот конкретный метод не работает так, как задуман». И ничего больше. Если фейл модульного теста не даёт такой конкретный ответ, то это плохой тест.
Интересно читать как что-то, предназначенное для отображения таблиц и их связей в объекты ЯП, чтобы ими можно было манипулировать как нативными, что-то там нарушает.
Нарушение, если оно есть, возникает тогда, когда код, реализующий отображение в объект, начинают расширять бизнес-логикой. Но нарушение ли это, если архитектор смотрит на таблицу с данными как на бизнес-модель? Если так, то тогда богатая бизнес-логикой модель AR точно отображает замысел архитектора.
Даже в мануалах по тестам мокают репу и возвращают готовую энтити.
Если же вам нужно проверить, что все связи настроены правильно, то это уже интеграционное тестирование, где вы проверяете, как меняется состояние при взаимодействии репы и базы: репа правильно заполняет энтити, и правильно пишет в базу.
В продакшен код, особенно в части взаимодействия по api, практически каждый второй пишет что-то, вроде
Или любой другой нужный тип. Так как используемая библиотека требует данных определённого типа. Без понимания, что преобразование типов — это не бесплатная фича динамической типизации. И потеря точности — меньшее из зол. Пример выше как раз о том, что мы таким образом может передать не те данные, что планировали.
Людей, которые сходу говорили, что float (числа с плавающей запятой) — это, вообще-то, диапазон значений, вообще, единицы. Не говоря уже о том, что физический смысл математических операций над float, — это совершенно не то, что многие себе представляют.
Нужно ответить, чему будет равно $b.
Лакмусовая бумага для выявления тех, кто не понимает, что он творит, занимаясь преобразованием типов.
Вот я и говорю, что у вашего класса довольно нетривиальный интерфейс. Шаг влево, шаг вправо — проведение объекта непредсказуемо.
Это не библиотечный функционал, а хелпер под конкретный, причем типовой, случай.
Т.е. выбранными вами архитектурными решениями, вы неявно резко ограничили применение классов исключительно рамками одной типовой задачи. Это решение может казаться хорошим, пока не захочется переиспользовать данные классы для чего-то другого. И тогда придется решать проблему отсутствия определенных гарантий у получаемых объектов, и весь код сразу станет не таким уж и классным.
Всё это делается средствами языка. Пример уже приводили. (type) — это каст. ?? — альтернатива, если null.
null ===
gettype / is_ — для получения и проверки типов.
А криминал в том, что для случае null вы, зачем-то, прогоняете альтернативное значение через объект, делая его интерфейс крайне нетривиальным. Например, в каком состоянии будет объект после второго применения default? Можно ли так вообще делать? Вызывая метод default, я откуда-то могу знать, что это объект больше никто не будет использовать? И т.д.
А если использовать генераторы, то и это ограничение можно снять
За велосипед с default хочется отправить почитать что-нибудь про Option. Не говоря уже о том, что ValueHandler объединяет в себе и конструктор объекта, и трансформацию, и хранилище. Жуткий класс. Хочется отдать его трем разным людям, чтобы каждый сделал свою часть правильно.
Нормальный этап взросления.
Сначала языковая конструкция кажется ребусом.
Пишется своя реализация, удобная, читаемая.
Потом добавляется в неё сахарок… И где-то начинают крутиться мысли, а что делает мой велосипед такого, что я не использую встроенные в язык конструкции?
?? кинет ворнинг при отсутствии элемента с таким ключом — это фиаско.
Использовать тайп каст, чтобы передавать в функции значения нужных типов — это эпик фейл.
Практически любой сериалайзер сделает всё тоже самое, что и библиотека автора, только скажи в какой объект сложить данные. И к преобразованию типов отнесется со всей серьёзностью, с предсказуемыми ошибками, если данные без потерь не кастятся в целевой тип.
Чем там гордиться? Тем, что такую специализированную оптимизацию поддерживать — запредельное зло?
Это надо самому поработать с таким кодом, доставшимся в наследство. Вместо ясной-понятной логики с обработкой крайних случаев, одна сплошная мешанина, где на каждый чих — уникальное решение.
Смотришь на такое и понимаешь: "работает — не трожь". Слишком долго и дорого это менять
А когда читаешь говно-код мастера «вам шашечки или ехать», то сразу возникают вопросы: а что это за валидация такая интересная, что это за бест-практики тестирования такие, почему нельзя было использовать стандартный логер? И самая главная головная боль: теперь надо будет учить (тратить время) других, чтобы они разбирались в этих выдающихся инженерных решениях, не имеющих ни документации, ни известных за многолетный опыт использования сообществом знаний о преимуществах и недостатках данных конкретных решений.
Намного выгоднее, используя в проекте конкретный фреймфорк, дать время новичку освоить его практики, а не позволять тащить бекграунд его предыдущих проектов сюда.
Опытный и так себе не позволит, работая в рамках фрейморка, изобретать велосипед для того, что итак уже в нём реализовано. Даже если он раньше с этим не сталкивался. В этом его ключевое отличие от новичка — не тащить за собой свои «достижения» из компании в компанию: я на прошлом месте работы так делал!
And now you shuffle the rows:
Oops.
Actually we have random order only for a column. On b and c we have a preordered values with the same random() value on each column.
That is because an expression in the order by clause is calculated one time for row.
Here is an error. Rows won't be inserted in a random order which is expected.
Подпишусь под записыванием за преподом.
Хоть свой пример не показателен, но имперически для себя выяснил, что переписывание материала — лучший способ его забыть. Запоминается он, только когда начинаешь с ним работать, крутишь под разными углами и отсутствие источника ответов на возникающие вопросы заставляет погружаться в материал. Для меня лучшим подспорьем от лекции — это список литературы, где изучить тему, и список вопросов, понимание которых свидетельствует, что разобрался в теме.
Те же вопросы и для функции decrypt? Вы хотите подтвердить, что эта функция может расшифровать ответ от вашей функции encrypt? Или расшифровать сообщение, зашифрованное какой-то сторонней библиотекой?
Какой контракт мы хотим подтвердить своим тестом? Что клиент расшифрует данные, которые мы ему отправим? Т.е. мы проверяем то, что данные, которым мы отправим, могут быть расшифрованы.
Тест тут прост и ясен:
encryptedData = encrypt(expectedData)
assertDecrypt(expectedData, encryptedData);
В assertDecrypt мы прячем реализацию decrypt, которую рассматриваем как эталонную: наш контракт таков, что зашифрованные данные будут расшифрованы данной реализацией. Используйте её или совместимую. Мы проверяем, что наша функция encrypt возвращает данные, которые могут быть расшифрованы этой эталонной реализацией.
Если мы хотим проверить, что мы можем расшифровать некие зашифрованные данные, то подход тот же:
decryptedData = decrypt(encryptedData);
assertEqual(expectedData, decryptedData);
Здесь мы проверяем, что наша реализация получает правильные данные, расшифровывая некие зашифрованные данные.
Но вот чего точно нельзя делать, так это взаимно использовать decrypt <-> encrypt в рамках одного теста, так как вместо валидации контракта, мы валидируем обратимость функций, о чём тут уже упомянули.
Вопрос в том, какую конкретно задачу вы хотите решить тестами?
Если проверять, что методы правильно раскладывают данные в базе данных, то надо делать тестовую бд, in-memory либо ещё как-то.
Если проверять, что методы правильно подготавливают данные перед тем, как они сохраняются через орм в бд, то нужно эти методы отделить от орм в отдельный слой и тестировать их. А орм использовать как зависимость, вроде репозитория.
И т.д. Сначала, что именно хочется проверять и зачем, потом реализация архитектуры под эту задачу.
Начните с простого.
Выберите какой-нибудь простой класс, с наименьшим количеством зависимостей и простой функциональностью, и напишите на его интерфейс модульные тесты.
Дальнейшее движение по более сложным классам вскроет довольно много интересного в используемой архитектуре, когда вы чуть ли не каждый метод каждого класса будете распознавать как for single use only. Т.е. из-за сильной связанности данный метод применим (полезен) только в данной конкретной ситуации, и развязать весь контекст, чтобы изолированно протестировать этот метод, не так уж и просто.
Такой вот спагети-код, когда что-либо конкретное в системе расширить довольно сложно, кроме как написать новую спагетину, пропуская её через весь код.
Практика написания юнит тестов добавляет навык видеть подобный код. Он может быть красивым и стройным, с маленькими понятными классами, но архитектурно не переиспользуемым.
Другим запахом такого кода являются мини итеграционные тесты, когда мы не можем написать на класс модульные тесты из-за связанности контекстов, и вынуждены писать тест, проверяющий цепочку вызовов.
Для этого существуют фикстуры. Готовим нужное состояние системы под каждое действие и проверяем, что оно работает так, как задумано.
Иначе мы просто впустую тратим время, инициализируя всё необходимое для всей цепочки вызовов, чтобы нужные состояния создавались самой системой, а не были захардкожены. Это тоже test smell, когда методы тестируются не изолированно друг от друга. И ещё один способ получить флаки тесты.
При этом, когда такой связанный тест упадёт, мы сразу поймём, что его архитектура (связанность) бесполезна для нас, так как в процессе починки такого теста, его придётся разбивать на части, чтобы понять, что именно пошло не так.
Хороший модульный тест своим фейлом говорит, что «вот этот конкретный метод не работает так, как задуман». И ничего больше. Если фейл модульного теста не даёт такой конкретный ответ, то это плохой тест.
Интересно читать как что-то, предназначенное для отображения таблиц и их связей в объекты ЯП, чтобы ими можно было манипулировать как нативными, что-то там нарушает.
Нарушение, если оно есть, возникает тогда, когда код, реализующий отображение в объект, начинают расширять бизнес-логикой. Но нарушение ли это, если архитектор смотрит на таблицу с данными как на бизнес-модель? Если так, то тогда богатая бизнес-логикой модель AR точно отображает замысел архитектора.