Pull to refresh

Comments 48

Тест работающий с базой данных не является юнит тестом, вы просто не сможете дождаться результата их работы. Назовите их функциональными тестами.
То что тесты работающие с базой данных прям такие медленные, что их нельзя использовать это совсем не правда. Типичная страшилка. Они медленные только по сравнению с тестами, не работабщими с базой. Просто посчитайте: несколько инсертов, апдейтов и селектов средней сложности это десятки миллесекунд не больше (в тестовой БД, обычно данных нет, при каждом тесте она обнуляется, или просто каждый тест идёт в отдельной транзакции). Оверхед на один тестовой пример небольшой, и хотя примеров много, в целом разница не смертельная, это еденицы минут для очень больших тестовых пакет и секунды, десятки секунд для небольших и средних проектов. Я не говорю что все тесты, должны работать с бд. Если в тесте можно обойтись без неё, то нужно обойтись. Но то как проект работает с БД тоже надо тестировать. И в нормальном тестовом пакете всё равно придёться иметь такие тесты, иначе регрессивность пакета будет никуда не годной.

P.S. А если тесты запускать на БД в памяти то оверхед вообще минимальным будет.
это теория, на практике тесты базы ставят крест на юнит тестировании уже через несколько недель после начала разработки
Да бросьте вы уже людей пугать. Что это за страшные тормоза от использования базы в текстах? Тестовая база на то и тестовая, чтобы быть пустой, маленькой и юркой. Ну и таблицы, расположенные в памяти, говорят, придумали.
Можно пример случаев. когда оно тормозит. Только не высосанных из пальца, а действительно актуальных?
У меня абсолютно другой практический опыт. Как и у многих других в rails сообществе. Сейчас, наоборот, всё больше говорят, что предпочтительно тестировать не стабя вызовы к БД. Выгода от менее хрупких и более надёжгых тестов стоит потерь скорости.
~1k тестов для Doctrine, примерно половина из которых работают с БД, выполняются за ~15 секунд на обычной машине.
А можно глупый вопрос — зачем?
Чем проверка
$this->assertEquals(3, $this->getConnection()->getRowCount('post'));

лучше проверки
$this->assertEquals(3, $blog->getPostsCount());

?
Объект $blog может ошибаться.
Это как?
Всегда возвращать 3? Проверяем исходное количество записей, добавляем еще одну, проверяем, что количество записей увеличилось на 1.
Не сохранять в базе? А мне пофигу ГДЕ он хранит, мне нужно чтобы он хранил.
   $blog = new Blog();
   $this->assertEquals(0, $blog->getPostsCount());

   $blog->addPost("My third post.", "This is my third post.");
   $blog->save();

   $loaded_blog = Blog::loadById($blog->getId());
   $this->assertEquals(1, $blog->getPostsCount());
Если заново перезагружать объект, то ничем не лучше, по сути. Если не перезагружать, то объект может ошибаться (добавить пост во внутреннюю коллекцию, но запрос на вставку сфейлится, например)
Лучше тем, что тестируется поведение, а не внутренняя реализация. Ну и отсутствием лишней библиотеки.
Вот так вот и плодятся массовые заблуждения относительно юнитетестов, из которых потом вырастает убежденность что модульное тестирование это «долго и дорого».

Контролллер -> Фасад -> микс из доменных объектов и сервисов -> Репозиторий/ДАО ->реляционнаясубд/key-value хранилище/in-memory/ file/etc

Так вот юниттест пишется Для! интерфейса! вашего хранилища/dao. Он независим от вашей реализации и работает всегда одинаково. (И когда вы смените реализацию — поможет убедиться что ничего не поменялось) Почти все тесты для него пишут так же как в младших классах нас учили проверять результаты вычислений: результат умножения проверяется делением, сложения — вычитанием. Ну или чуть более формально: f^-1(f(A))==A

Для тестирования «другой» стороны — используются разного рода «пустышки».
И всё. Никаких БД. (если конечно это МОДУЛЬНОЕ тестирование)
Весьма верное замечание. Позвольте пожать вашу руку, коллега!
Я согласен. Но, бывают исключения.
Например, то, почему я связался с юнит-тестированием БД. Есть система, написанная и спроектированная в ~2004 году без всяких DAO. Система большая и работает (с костылями, но работает). Нас, как разработчиков она не устраивает, но она устраивает бизнес, потому что _работает_. Времени и средств на переписывание с нуля естественно никто не выделяет по причине «оно и так работает». Мы хотим «переписать» систему своими силами в процессе выполнения тасков. Для этого нужно покрыть все юнит-тестами.
Вам важен факт факт, что она работает, или как столбцы называются? Зачем вводить зависимость от SQL?
у них другая проблема.
У них есть монолитная система. Скорей всего — достаточно сложная (простую систему можно было бы быстро переписать).
Есть потребность навести в системе порядок. Хотя бы минимальный.
Каждое изменение сопряжено с появлением новых ошибок (много связей + языки с динамической типизацией помогают).
Единственные стабильные интерфейсы в такой ситуации это со стороны пользователя (напр. контроллеры в MVC) и со стороны базы. Все остальные интерфейсы будут меняться в процессе рефаткоринга.
Вот и они и стремятся написать автотесты через те стабильные интерфейсы что у них есть, чтобы во время рефаткоринга получать информацию что «вроде работает так же ка краньше». Других враиантов кроме как «с базой» у них особо нет.

просто такие тесты — совсем не «юнит».
Это же веб, т.е. уже не полностью монолит. Я бы начинал сверху, с уровня контроллеров/экшенов и спускался вниз, выделяя слои. В любом случае — контроль над базой им особенно ничего не даст, кроме документирования работы с базой текущего кода.
Хотя согласен, это будет полезно, если переписывать код целыми модулями. Если же править итерационно, то не вижу плюсов.
Ну представте, что у вас и «бизнесс логика» закодирована прямо в экшенах контроллера и всё это выглядит например так:

валидация данных из $_POST
чтение чегото из базы (вот прямо mysql_query)
какие-то манипуляции с прочитанными данными и данными из базы (прямо здесь же. в лучшем случае в приватных методах/функциях)
запись данных в базу (всё тем же mysql_query)
вывод результатов (часто с ипользованием чего-нибудь типа Smarty) )

В такой ситуации просто нет других интерфейсов на которые можно было бы опереться при тестировании
Вы же сами говорили о проверках умножения, через деление. Находим проверочное действие, глобально отметаем представление, подменой Smarty-объекта на свой, тестируем связку действий. Интерфейсов как минимум два — http и смарти. Контроль над базой не даст протестировать чтение, только create/update/delete.
в том контексте который я описал выше — всё становится с ног на голову)
в модульном тестировании — всё достаточно просто и тестировать работу операций действительно удобно попарно с их обратными «функциями». Но такие случаи — не модульное тестирование)

Когда у нас есть огромный кусок кода, у которого много обязанностей (да и чего уж там — иной раз просто непонятно что этот кусок в точности делает) — тут тестировать обратными функциями конечно можно, но это стоит дороже.
Нам нужно добавить например 100 сущностный что бы проверить операцию чтения. Да причём ещё не рэндомных — а сущностей с некоторыми условиями. А когда вы не знаете что именно делает этот код — это становится сложно — требуется сначала разобраться, затем или написать какой-то генератор или руками нагенерить данные. Это всё требует времени и так же сопряжено с ошибками.

С другой же стороны — у нас уже есть набор данных в базе — на котором работает наша система. Есть собственно сама рабочая система. В данной ситуации выглядит более разумным сделать примерно следующее:

0)«Оторвать» представление дабы мы могли внедрить туда нужный нам объект.
1)Делаем дамп боевой базы
2)ждём 1-2-3 дня (попутно собирая подробные аксеслоги. не забываем про параметры POST)
(3) опционально -Делаем новый дамп базы.

Далее делается 2 тестовых инстанса:
1)работающая сейчас система
2)система на которой мы проводим рефакторинг

каждой из них даётся слепок базы из п. (1)
и начинают накатываться запросы из аксеслога (оригинальный или модифициорованный)

внедренные вместо шаблонизатора заглушки повзволяют нам сравнивать какие данные хотят вывести обе системы.

как вариант используем дамп (3) и сравниваем с тем что получилось у нас. (эффекты связанные со временем или игнорируются или учитываются специальным образом по мере обнаружения) )

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

вот как-то так.
ну и ещё раз — это не модульное тестирование и здесь «всё по другому»)
Ну да, фиксация состояия, я уже выше согласился))

В такой же ситуации я начинал с пустой(ну кроме словарей) базой, и прошелся по всем прецедентам, логгируя связки action -> {sql-запросы}. После недолгой медитации над результатами, удалось узнать возможные side-эффекты от изменений. Собственно отсюда и были получены связки action'ов, которые можно тестировать друг другом.

И все равно я не понимаю зачем эта библиотека, если мы оба говорим о слепках :)
Равное тестируют, либо равным, либо более низкоуровневым. Иметь еще одну библиотеку для работы с sql? Плюсы? Смены БД на непонятном legacy-коде не будет. Банальное неудобство mysql_*? Есть PDO. Дампы проще снимать и применять в SQL. Да и читается он лучше, чем XML.
Почему не даст? Заносим в базу данные из фикстур, выполняем «юнит» с функцией read, проверяем его результат на соответствие тому, что занесли (вплоть до проверки через DOM и/или регулярками выходного html, захваченного через ob_*), чистим базу.

Сам сейчас начал рефакторить очень монолитный проект: каждый «экшн» это обычный спагетти-код, где вывод html и логика перепутаны, нет даже намёка на отделение логики от представления, почти каждый экшен начинается с <html> и заканчивается </html>, инклудится, по сути, только mysql_connect и mysql_select_db. Изменения в дизайн приходится вносить правкой в сотне файлов. Естественно первый позыв вынести хотя бы общий для каждого экшена код (прежде всего html c хидерами/футерами) за скобки, но хочется быть уверенным, что ничего не сломалось в процессе выноса (в сотне файлов проделать десяток операций по копипасту очень занудно, и легко что-то забыть из-за отупления).
Всю эту штуку использовать только для заливки фикстур? Как-то расточительно ;)
Что значит только для заливки текстур? Стандартный xUnit воркфлоу — подготавливаем юнит (заливаем в базу фикстуры, устанавливаем переменные и т. п.), выполняем юнит (инклудим скрипт, перехватывая вывод, стандартный PHPUnit_Extensions_OutputTestCase мне неудобным показался, потому тупо через ob_* перехватываю), затем ассерты (в основном assertSelect* и assertTag) и очистка.

А уже покрыв «юнит» (скрипт) тестами, можно спокойно начинать его рефакторить, при этом изначальные тесты служат в качестве интеграционных.
Если бы представление было хотя бы на смарти, было бы намного легче. То, что сейчас у нас, лучше не показывать неподготовленному человеку, знакомому хоть немного с MVC (:
Как я вас понимаю… Код, с которым сейчас работаю, лучше не показывать даже тем, кто хоть как-то привык повторно использовать код, даже презираемые многими include 'header.phtml';… include 'footer.phtml' показались бы мне, наверное, идеалом кодирования
Да. Всё верно.
Но просто дело в том что в вашем случае это не ЮНИТ-тесты.
Плохо спроектированные системы не поддаются модульному тестированию. Автотесты которые при определённых усилиях можно для таких систем написать — как правило большие, сложные (сложность часто сравнима с тестируемой подсистемой) и очень хрупкие. Они долго пишутся, часто исправляются.
Это может называться функциональными или интеграционными тестами в зависимости от масштаба бедствия. но никак не модульными.
юнит-тест является автотестом. обратное верно не всегда. И распространение материалов в которых автотесты «вообще» названы модульными — формирует неправильное представление у наших с вами коллег вообще (и связаных с php в частности) и в конечном итоге способствует увеличению числа проектов вроде того в котором оказались вы.

Кстати — не думали о смене места работы?
Учиться новому и перенимать лучше практики — луче же чем исправлять чужие ошибки. Тем более что такая работа часто бывает невидна и непонятна «обычным людям» (читай представителям бизнеса)
Спасибо за Ваши комментарии, они действительно в самую точку :) Название статьи я подправил.
Касательно смены работы — думал об этом раньше, сейчас у нас наметились сдвиги в лучшую сторону, и очень хочется поучаствовать в этом :) ну а best practices хватает благодаря сторонним проектам на zf и magento.
Да, большинство сервисов должно тестироваться с застабленными репозиториями, но ведь это не отменяет необходимости интеграционного тестирования, когда те же сервисы работают с реальной базой. По сути, единственная ошибка автора в том, что он интеграционные тесты назвал юнит-тестами.
да. полностью согласен. и собственно об этом я и написал.
О терминологической неточности которая вводит в заблуждение коллег по цеху.
YAML либо JSON хорошо подходит для описания данных. Таблицы типа memory в MySQL отстреливают ОЧЕНЬ быстро, нам не надо мучать диск. Кстати — пока неясно как через такие описательные языки создавать внешние ключи и вязать таблицы, поэтому мы покаструктуру накатываем простыми SQL, а вот данные грузим из YAML.
Механизм fixtures описанный в этой статье это плохо. Фикстуры ненадёжны и неудобны. Это поняли ещё в 80-х. Rails сообщество поняло пару лет назад. Php ещё нет :-). Надо пользоваться фабриками. Не знаю как с ними обстоит в php, видимо, плохо.
Это понял автор статьи. Раз пока нет широкоиспользуемых решений для этого подхода, значит пока не поняло.
А какие решения? Вся суть паттерна: не хочешь дублирования бизнес-правил в снимке базы и коде — используй бизнес код для наполнения данных. Ну можно генераторы строк туда прекрутить. Генераторы файлов. А в остальном то все руками пишется.
А это только так кажется, на самом деле, если писать создание тестовых объектов руками, то будет много дублирования. Сложный объект не просто создать, у него много связей. А что если какие-то аттрибуты у модели уникальны: вручную следить за их уникальностью?
Без factory_girl (или чего то такого) github.com/thoughtbot/factory_girl мне было бы очень трудно. Например, можно создать базовое правило производства объекта, а затем конкретизировать его в новом правиле, наследовав от старого.
Как то так?
function createPersonWithJob($finance)
{
  $person = $this->createPerson($dob = null, $with_passport = false, $name = null, $finance);
  return $person;
}
$this->creator->createAndLoginUser()
Не знал, что это называется ObjectMother. Мы пришли к подобному паттерну, пытаясь абстрагироваться от часто повторяемых в тестах пользовательских сценариев (залогиниться, создать документ и т.п.). Поэтому называем такие фабрики UserScenario.

Только вот на душе остается какой-то не хороший осадок. Ведь эти «мамы», для того, чтобы рожать каких-то, прямо скажем, незапланированных детей, начинают «залетать» изрядными кусками сервисной логики. Причем эти куски, по своему характеру, очень напоминают соседей из business-logic — хоть тесты на них самих теперь пиши.
А вот этого не должно быть. С какими частями дублируется код? С контроллерами?
Например, сценария createAndLoginUser() в реальном коде не существует — потому, что пользователей заводит администратор, но ни когда их не логинит. Логиняться пользователи всегда сами, но только в существующий аккаунт.

Однако в тестах очень часто участвуют именно такие вот залогиненные пользователи, поэтому от деталей их создания и логина хочется абстрагироваться. Думаю, такой код мог бы быть контроллером (если бы необходимость в такой логике, по факту, была не только в тестах).
А в чем проблема. Код создания пользователя есть, код логина — тоже. Остается объединить. В идеале:
function createAndLoginUser($params = ...)
{
  $auth_manager = new AuthManager($this->createUser($params));
  $auth_manager->login();
}

У нас, кстати, loginByUser используется повсеместно. Администратор может залогиниться под любым пользователем. Помогает при расследовании баг-репортов.
Значит в вашей системе эта логика является вполне нормальной — бизнес- — наверняка, окруженная полноценными тестами, документацией и проч. семейной атрибутикой. Нашим администраторам такая возможность не требуется. В итоге аналогичный код одиноко подвисает в слое тестов, в виде абсолютно неконтролируемой логики (чуть не сказал — женской). Получается, что интерпретация зависит от конкретного набора требований.

Так вот проблема — нужно-ли писать тесты для MotherObject или нет? :) Голос «за» — они рожают точно такие же объекты, какие появляются на свет у нормальной бизнес-логики (просто какими-то левыми сценариями). Голос «против» — кажется, это будут тесты, ради тестов.
По идее, логика ObjectMother должна быть тривиальной — либо простое конструирование объекта(ов) и установка его(их) атрибутов, либо обращение к уже протестированным конструкторам, сеттерам, фабричным методам и т. д. нормальной бизнес-логики, ветвления там, по-моему, неуместны, линейное выполнение, разве что в примитивный цикл свёрнутое. А появляться «мама» должна в процессе рефакторинга работающих тестов (опять-таки тривиального рефакторинга), а значит тестами для «мамы» будут служить и уже имеющиеся тесты, и тестируемая логика. Неправильно написали «маму» — тесты посыпятся, они из «юнит» превращаются как бы в интеграционные («мамы» и тестируемого юнита), но раз логика мамы тривиальна, то фактически остаются юнит-тестами юнитов :). Как-то так, по-моему.
Спасибо, теперь понял.
>Rails сообщество поняло пару лет назад.

Наверное именно поэтому в гайдах к рельсам фикстурам выделена треть главы, а Factory Girl и
Machinist по строчке :)
Когда таблица оказывается пустая, DbUnit вываливается с ошибкой. github.com/sebastianbergmann/dbunit/issues#issue/11

Отправил патч/пулл полгода назад, но пока что не торопятся его тестить/добавлять. Так что, кому надо, патчите руками.

И еще. Большой косяк вылазит на практике, когда у вас есть колонки типа timestamp или каким либо другим образом скрипты каждый раз вставляют туда разные даты, например, если вы тестируете регистрацию пользователей.
О проблемах со временем я упомянул практически в самом конце :)
С виду такая хорошая, годная статья, но такая длинная что я уже третий день не могу ее начать читать.
Sign up to leave a comment.

Articles