Pull to refresh

Comments 37

UFO just landed and posted this here
Меня всегда пугала формулировка про «минимальную реализацию». Минимальная реализация — это, может быть, захардкодить ответ на тестовые значения. Я понимаю, что подход предполагает не это, поэтому, думается, надо формулировку как-то переосмыслить.
При проектировании метода вы пишите первый тест, где на входном значении (2,2) вы ожидаете 4. В реализации вы хардкодите return 4. Это третий принцип TDD. Вы пишите только тот минимальный код реализации, чтобы текущий тест прошел вместе со всеми остальными. Потом вы пишите ещё один тест с входными значениями (1,0) ожидая return 1, и уже пишите код сложения.

Ещё один момент: чтобы не словить ситуацию когда вы подбираете и бесконечно пишите бесполезные тесты вроде (1,0), (1,1), (1,2) и т.д, тест после написания обязательно должен провалиться (второй принцип TDD). Если тест после написания не проваливается значит он не нужен.
и уже пишите код сложения

Или хардкожу 1+0=1, и все заново — речь про это.
Нет, в данном случае не пройдет (другой тест проверяет что функция должна вернуть 4), но это очень важный момент!

У вас выбор: написать if в зависимости от входного значения, либо написать функцию сложения. Тут надо опять же руководствоваться принципом минимальной реализации. Написать функцию сложения проще чем бесконечно удовлетворять условиям однотипных тестов.

Помните, вы пишите тесты перед реализацией для того чтобы облегчить себе жизнь. Вам не нужно тестировать в стиле «если функция работает с (1,1) то не факт что она будет работать с (100,100), напишу ка я тест». Вы понимаете что вам для удобства и удовлетворения требований бизнеса необходимо от метода конкретное поведение. Вы его фиксируете тестом. Если хотите протестировать ещё и пограничные состояния вы делаете ставку и пишите тест. Если тест в будущем сломается вы выиграли. Если нет, то тест оказался бесполезным. В любом случае тест должен обязательно провалиться, иначе это стопроцентно бесполезный тест.
Я все это прекрасно понимаю :). Я говорю о том, что формулировке «пишите минимальную реализацию» явно не хватает кое-каких деталей (примерно тех, которые вы сейчас добавляете).
Вряд ли вы можете утверждать, что реализация вида
if (a == 0 && b == 0) return 0;
if (a == 1 && b == 0) return 2;
//Повторить для всех a, b принаждежащих R

будет проще return a + b;

В этом смысле, a + b — и есть минимальная реализация, и это станет понятно после того, как вы напишете три теста, складывающих разные числа. И если строго следовать принципам, вы должны будете «зарефакторить» кучу if-else в сложение только после пары тестов. на практике получается, что большая часть программистов — не идиоты и сразу видят тенденцию, заменяя операцию на сложение уже после первой реализации.
К слову о переборе всех частных случаев. Одним из важных требований TDD, предотвращающих «застревание» разработчика является требование о том, что в стадии Green (написании кода реализации) по возможности нужно повышать уровень абстракции решения (ну или в худшем случае, не уменьшать его), что противоречит такому подходу к разработке как «перебрать всевозможные решения».

Роберт Си. Мартин в своей серии видеоуроков «Clean Code» (В эпизоде «Advanced TDD») говорил об этих принципах и рассказывал о том, о чем умалчивают три постулата TDD.
Да-да, я ровно об этом: явно нужны какие-то дополнительные пояснения, ибо постулаты слишком уж о много о чем умалчивают. Эта статья, кстати, как раз этим и занимается: вносит дополнительные пояснения к постулатам test-first.
Не будем лукавить — каждую новую строчку в эту череду if-oв, вообще говоря, добавлять проще, чем вот это общее «а+b». Здесь просто вырожденный случай, функция "+" уже написана, так что ее вызов получается короче, чем расширение хардкодингом. Обычно же, все наоборот, пишут функции которых еще нету, так что придется поднапрячься, что приведет к нарушению принципа «самая простая реализация», которая удовлетворяла бы тестам.
Вы можете возразить, что «это должно быть сделано на стадии рефакторинга, где это требование уже не действует», но тогда мы возвращаемся сразу к нашей статье, где и сказано о том(в терминах TDD), что непонятно, а после скольких тестов и захардкоженных ветвлений, нужно этим рефакторингом заниматься. Ведь с такой трактовкой, как ваша, настоящая программистская работа, where rubber hits the ground, где мы пишем алгоритм, происходит именно на стадии рефакторинга. И это все равно сводится к тому, что нужное количество тестов будет очевидно после фазы рефакторинга, что будет нарушением test-first. Это точно та же проблема, но просто несколько отложенная.
Ведь с такой трактовкой, как ваша, настоящая программистская работа, where rubber hits the ground, где мы пишем алгоритм, происходит именно на стадии рефакторинга.


Нет. Рефакторинг подразумевает изменение структуры программы без изменения её поведения. На стадии рефакторинга вы не можете писать новые тесты по определению. Настоящая работа происходит на всех этапах.

… что непонятно, а после скольких тестов и захардкоженных ветвлений, нужно этим рефакторингом заниматься


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

Не будем лукавить — каждую новую строчку в эту череду if-oв, вообще говоря, добавлять проще, чем вот это общее «а+b».


Не проще, потому что у вас дедлайн и вам платят за разработку ценного функционала.

TDD это строгая дисциплина и она требует определенного последовательного процесса работы. Я сам прошел через это. «у меня метод уже полностью в голове реализован и он точно будет корректен, зачем писать эту пачку бесполезных тестов?». Лень. Медленные интегрированные тесты, когда желания писать много тестов нет вообще. И минимальная реализация с хардкодом при первом тесте, это же для дураков ведь, да? Я же умный, могу писать правильную реализацию сразу и очнуться через пол-часа с десятью интегрированными тестами, которые не принуждают дизайн системы быть удобным для тестирования, не дают по нему никакой обратной связи и не отлавливают все ошибки в коде. Зато «сэкономил» 15 минут, о да.
UFO just landed and posted this here
Как правило, в современной промышленной разработке теоретическое доказательство верности программ (а) практически невозможно и (б) не требуется.


Что такое «теоретическое доказательство верности программ»?

В условиях, когда каждый тест перепроверяет функцию целиком, без знания о ее зависимостях и внутреннем устройстве, тест действительно сообщает, работает ли данная функция.


… и это действительно очень плохо.

Однако, напоминаю, мы имеем дело с тестированием черного ящика, а значит, мы не знаем, будет ли использована функция is_german_letter. Но это знание играет решающую роль в выборе наборов входных данных, о которых говорилось выше.


Почему у вас входные данные зависят от реализации метода, а не наоборот? Почему имеет значение какой метод был вызван для получения ожидаемого тестом результата?

1. Ненаписанную функцию можно тестировать только как черный ящик
2. Ненаписанную функцию не стоит тестировать как черный ящик


Я вижу тут или противоречие, или «аргумент» против TDD.

Но при тестировании методом черного ящика, игнорируя возможное наличие классов эквивалентности, никаким нормальным образом выбрать эти примеры вы не сможете — их будет или слишком много, и будет делаться лишняя работа, или слишком мало, и тестирование будет не закончено.


Нормальным образом можно это зафиксировать написав все тесты строго до реализации.
Что такое «теоретическое доказательство верности программ»?

Формальная_верификация

Почему у вас входные данные зависят от реализации метода, а не наоборот?
Нормальным образом можно это зафиксировать написав все тесты строго до реализации.

Вот об этом как раз статья. Зафиксировать столько, сколько нужно раньше реализации — не получается.

Я вижу тут или противоречие, или «аргумент» против TDD.

Почти: это аргумент против test-first в его классическом понимании, да.
Спасибо за формальную верификацию, изучу вопрос.

Зафиксировать столько, сколько нужно раньше реализации — не получается.

Почему бы не фиксировать одно требование за раз?

черт, промазал
Спасибо за формальное описание проблем с тестированием «чёрного ящика», классы эквивалентности и вот это вот всё.
Спасибо! Я ждал вашего комментария, рад, что понравилось :).
В примере с number_of_german_letters нужно считать функцию is_german_letter частью ее интерфейса. Т.е. это функция высшего порядка с двумя аргументами: функцией определения буквы и строкой. Интерфейс будет что-то типа
number_of_specific_letters (is_specific_letter, s)
а вызывать ее будем как
number_of_specific_letters (is_german_letter, «abc1»)
Тогда можно рассматривать ее как черный ящик?
Это любопытный взгляд на вещи, но легко видеть, что с ним в терминах статьи все еще хуже: в этом случае даже интерфейс до написания функции неизвестен (т. к. то, что надо использовать is_german_letter, я придумаю только на стадии реализации).

А вот в процессе реализации как раз все становится, как вы говорите. Иначе это можно выразить так: формулировка функции с «посчитай немецкий буквы» меняется на «посчитай буквы, которые is_german_letter».
Да, согласен, хотя это немного другой вопрос — разбиение приложения на функции. Конечно, нужна какая-то итеративность тест-код-тест-код, невозможно заранее написать тесты для большого куска приложения.
Не получится еще по одной причине. «is_german_letter» мы выбрали как самый характерный пример, но то же верно относительно всех остальных используемых в ней функций. Там, по массиву наверняка придется поитерировать, числа поскладывать. И каждый раз мы упираемся в то, что "+" мы доверяем и итератеру мы тоже доверяем, что опять повлияет на итоговый выбор тестов, а следовательно затея с черным ящиком опять провалилась.

Плюс, заменив техзадание на «напишите number_of_specific_letters» — мы даже с is_german_letter проблему не решаем. Мы просто подвинули ее на 1 уровень выше, в том место, где это number_of_specific_letters будет вызываться. Там точно также встанет вопрос, доверяем ли мы is_german_letters, которое использовали как параметр.
В примере с number_of_german_letters прежде всего нужно решить является ли функция is_german_letter внутренней декомпозицией логики функции number_of_german_letters или она является полноценной частью API разрабатываемого модуля.

Если полноценная функция, то тестировать её нужно отдельно, а в (юнит-)тестах number_of_german_letters — стабить/мокать. Выносить её как явную зависимость или переопределять, пользуясь возможностями языка/фреймворка — чисто тактическое решение.
Одна из основных проблем при попытках применении test-first — стремление (у кого сознательное, у кого не очень) заменить тестами формальную верификацию, покрыть все различные варианты, все классы эквивалентности, нарушая принцип черного ящика (даже если реализация ещё только в голове). То есть не падающие тесты рассматриваются как доказательство работоспособности программы.

Основное же назначение же тестов при test-first сначала (когда падает) — формальное доказательство отсутствия требуемой функциональности в каких-то частных случаях, а потом (когда проходит) — фиксация её наличия в этих случаях. При test-first не нужно писать тесты, которые почти гарантировано заработают без изменения кода. С другой стороны, не нужно писать код, отсутствие которого не повлияет на прохождение тестов (рефакторинг таким кодом не считается). Разработку теста следует рассматривать как формализацию нового требования (баг-репорта или фиче-реквеста), откуда-то (абсолютно не важно откуда, может фантазия заказчика, может анализ кода как белого ящика) полученного. Часто можно услышать «когда пишешь тест, представь себя тестировщиком», что сильно мешает, поскольку задача тестировщика (в представлении обычного разработчика :) ) — выявить ошибки, что неверно. Тестировщик при автоматическом тестировании — это тестирующий фреймворк :). Когда же придумываешь и пишешь тест, то представлять себя надо аналитиком, постановщиком задачи и тестировщику, и разработчику. Тогда многое становится на свои места.
UFO just landed and posted this here
Спасибо! Учтите, что test-first ≠ TDD :).
Мне всё время было интересно как это, тестирование по принципу черного ящика. Везде пишут простые примеры сложения и перемножения переменых, а как на счёт чего-то действительно сложного, например:

  • Рассылка письма из параметров функции по базе из 100500 email адресов;
  • Парсинг стороннего RSS и сохранение новостей в бд;
  • Push данных в стороннее API.


В случае например с email рассылкой. Понятно что функция использует какой-то драйвер БД и для отправки сообщения конкретному получателю используется какой-то сервис. Но если мы тестируем по принципу чёрного ящика, то мы ничего не знаем о внутренней реализации и завистмостях и мы должны вызывать эту функцию и проверять все 100500 ящиков.

Понятно что в обычном тестировании мы можем мокнуть сервис отправки сообщений и драйвер БД, но в этом случае мы уже начинаем привязываться к реализации. Дальше больше. Не стоит забывать о том что в нормальных драйверах БД есть несколько способов получит данные, например в Doctrine:

  • Метод из репозитория;
  • ORM QueryBuilder;
  • DBAL QueryBuilder;
  • DQL;
  • SQL.


Любой из этих способов может быть применен для получения списка адресов. Если мы функцию тестируем как черный ящик мы не знаем как нам замокать драйвер БД так что бы он не тянул даные из БД, а пытался отправить письмо на нужные нам адреса. А в случае изменения метода получения адресов нам придется менять тест, что противоречит принципу черного ящика.

В случае test-first мы можем знать о том что методу понадобятся конкретные зависимости, но не имея реализации мы не можем точно знать как именно будут использоваться эти зависимости. Точнее в принципе мы можем предположить и зарание написать тест который полностью ограничивает реализацию, но это очень сложно. В этом случае гараздо проще написпть реализацию и потом по ней тест, чем тест по предпологаемой реализации, а потом реализацию по тесту и в процессе написании реализации выяснить что тест написан криво и с ошибками, написать нормальную реализауию и потом правит тест под нормальную реализацию.

Я себе представляю как я буду писать тест на ещё не созданый запрос построеный с помощью QueryBuilder.

PS: Если кому интересно, могу привести пример как запрос на 15 строк превращается в тест на 93 с жёсткой привязкой к порядку вызова методов.
Рассылка письма из параметров функции по базе из 100500 email адресов;

Тут, как минимум, две отдельные функции:
— рассылка письма по списку/массиву/коллекции/итератору/… адресов
— получение списка/массива/коллекции/итератора/… адресов

В случае test-first мы можем знать о том что методу понадобятся конкретные зависимости, но не имея реализации мы не можем точно знать как именно будут использоваться эти зависимости.

Как это не можем точно знать? Зачем мы их вводим, если не знаем как будем использовать?
Тут, как минимум, две отдельные функции:
— рассылка письма по списку/массиву/коллекции/итератору/… адресов
— получение списка/массива/коллекции/итератора/… адресов

В общем случае функция реализуется в 4 строчки. Выносит рассылку в отдельную функцию не совсем разумно, особенно если это единственное место в проекте где есть рассылка. Ну а даже если разносить, мы всегда можем подняться на уровень выше и встретить там все тоже самое. Например в контроллере приходит запрос от пользователя и далее:

  • мы валидируем данные от пользователя;
  • создает сообщение на основе данные от пользователя;
  • получаем список получаетелей сообщения;
  • отправляем каждому получателю сообщение;
  • отдаем пользователю результат отправки.


Я привожу очень условный пример. Понятно что если адресов 100500, то это как минимум cli команда, а в идеале еще и очередь используется что-то типа RabbitMQ, но суть то не в этом. Суть в том что функции высокого уровня абстракции может быть очень сложно протестировать по принципу black-box, а порой и вовсе невозможно.

Как это не можем точно знать? Зачем мы их вводим, если не знаем как будем использовать?

Пример я привел. Мы точно знаем что нам необходимо получить данные из бд, то есть будет использоваться драйвер бд (например EntityManager из Doctrine), но мы не знаем какой из методов получения будет использоваться и как. В некоторых случаях мы можем точно знать как будет использоваться зависимость, но не во всех.
Выносить рассылку в отдельную функцию не совсем разумно, особенно если это единственное место в проекте где есть рассылка

1. Не разумно выносить в функцию? Вам необходимо вводить отдельный класс, обязанностью которого будет осуществление рассылки и подключать его как зависимость.
2. Не единственное: тесты тоже будут использовать этот метод. Это также аргумент в сторону тех кто не хочет создавать интерфейсы для только одной реализации. Stubs тоже будут использовать этот интерфейс.

Если вы пишите тесты перед реализацией то реализация представляет собой чистейший черный ящик, потому что реализации ещё нет.
1. Не разумно выносить в функцию? Вам необходимо вводить отдельный класс, обязанностью которого будет осуществление рассылки и подключать его как зависимость.

А почему мне именно «необходимо» вводить отдельный класс. Если при рассылке делается что-то больше чем проход по циклу и передача каждого значения функции отправки, то да, скорей всего понадобится вводить еще один уровень абстракции.
Если ваша функция называется «Получить всех пользователей И отправить им сообщение», то нужно разделять, а если функция называется «Отправить все пользователям сообщение» то разносить нечего. Я согласен что здесь можно, а иногда и нужно, вводить еще один уровень абстракции, но почему вы считаете что это единственно верный способ? Так ли необходимо создавать еще один класс из-за этих 3 строчек:

foreach ($this->rep->getUserEmails() as $email) {
    $this->mailer->send($email, $message);
}


2. Не единственное: тесты тоже будут использовать этот метод. Это также аргумент в сторону тех кто не хочет создавать интерфейсы для только одной реализации. Stubs тоже будут использовать этот интерфейс.

Тесты будут использовать конечный метод. По сути нет разницы будешь ты тестировать получение списка email и рассылку сообщений по отдельности или в рамках одного метода. Скажу даже больше, код теста в обоих случаях будет схожий.

Если вы пишите тесты перед реализацией то реализация представляет собой чистейший черный ящик, потому что реализации ещё нет.

Тут вы не правы. Тестирование по принципу black-box означает тестирование реакции на воздействие абстрагированное от внутренней реализации. То есть мы тестируем только выходные значения для заданных входных.
В случае же test-first, мы действительно не знаем ничего о внутренней реализации, потому что ее еще нет, но мы можем управлять внутренней реализацией. Мокнуть сервис, проверить передаваемые ему параметры.

Говоря другими словами, в случае black-box если тестируемая функция записывает что то в БД, то мы должны проверить наличие этой записи в бд, а в случае test-first мы должны проверить что сервису БД были переданные данные на запись.
Я предполагал что код метода `send` находится прямо внутри метода отправки сообщений пользователей, у вас всё верно. Мои аргументы предназначались для случая когда отправка выполнялась прямо в методе.

а в случае test-first мы должны проверить что сервису БД были переданные данные на запись

Верно.

Тестирование по принципу black-box означает тестирование реакции на воздействие абстрагированное от внутренней реализации.

Вот кстати я не до конца понимаю этот момент. Если мы мочим реализацию public методов зависимостей, которые потом использует
тест, это считается black box или нет? С одной стороны мы предполагаем что должно происходить внутри метода, с другой стороны нас это не интересует, так как нам важно получить нужный assert и не важно как метод это будет делать.
Вот кстати я не до конца понимаю этот момент. Если мы мочим реализацию public методов зависимостей, которые потом использует
тест, это считается black box или нет? С одной стороны мы предполагаем что должно происходить внутри метода, с другой стороны нас это не интересует, так как нам важно получить нужный assert и не важно как метод это будет делать.

это считается white-box testing
Тут вы не правы. Тестирование по принципу black-box означает тестирование реакции на воздействие абстрагированное от внутренней реализации. То есть мы тестируем только выходные значения для заданных входных.
В случае же test-first, мы действительно не знаем ничего о внутренней реализации, потому что ее еще нет, но мы можем управлять внутренней реализацией. Мокнуть сервис, проверить передаваемые ему параметры.

Оба подхода прекрасно сочетаются, если считать вызов сервиса тестируемой выходной реакцией. То есть для метода из трёх строк типа:
public function SendMessageToAllUsers($message) 
{
  foreach ($this->rep->getUserEmails() as $email) {
    $this->mailer->send($email, $message);
  }
}

ожидаемой реакцией будет:
1. Вызов $this->rep->getUserEmails()
2. Вызов $this->mailer->send() для каждого из возвращенных в шаге 1 адреса

Мы не знаем как это будет реализовано (например, циклом, итератором или вообще рекурсией), поэтому это чёрный ящик для нас, но мы знаем, что вызов этих зависимостей есть цель написания данного метода, поэтому можем применить test-first. На уровне юнит-тестов мы мокаем репу и мэйлер и проверяем, что всё вызывается как нам надо. На уровне интеграционных тестов мы создаём в тестовой базе записи, создаём тестовый мэйлер, передаём их объекту, дергаем наш метод и сравниваем базу с логом мэйлера. На уровне функциональных — создаём пользователей, дергаем контроллер и проверяем почту юзеров. Главное не путать уровни и тогда у нас всегда будет чёрный ящик, пригодный для test-first. Мы не будем знать детали реализации, достаточно только интерфейса. На одном уровне интерфейс будет на уровне языка (вызов методов), а на другом на уровне пользователя (заполнили окошки, проверили почту).
Ну да. Понятно что в данном случае мы можем мокнуть наши сервисы, написать юнит-тест и применить test-first. Мне было интересно как применять black-box тестирование в данном случае. Получается нужно делать какие-то хуки на уровне окружения. Проверять лог отправки. В случае парсинга RSS и внешнего API подменять хост в hosts и дописывать дополнительный обработчик который будет отправлять/сохранять тестовые данные для подмененных внешних сервисов. Запускать придется в песочнице. А в случае запроса к сервисам по IP придется еще заморачиваться с перенаправлением трафика.
Та еще развлекуха.

На тему test-first. Я согласен что во многих случаях написать тест до реализации не проблема. Тут я не спорю. Я просто приведу пример простого на вид кода и его теста, возможно на их примере будет понятно что написать тест до реализации может быть не так уж и просто.

Тест
$channel = 'foo';
$date_start = new \DateTime('2016-01-11');
$date_end = clone $date_start;
$date_end->modify('+7 day');
$result = [['2016-01-11', 10]];

$sth = $this->getMockBuilder(Statement::class)
    ->disableOriginalConstructor()
    ->getMock();
$sth
    ->expects($this->once())
    ->method('fetchAll')
    ->will($this->returnValue($result));

$i = 0;
$builder = $this->getMockBuilder(QueryBuilderDBAL::class)
    ->disableOriginalConstructor()
    ->getMock();
$builder
    ->expects($this->at($i++))
    ->method('select')
    ->with('e.date, COUNT(*) AS `total`')
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('from')
    ->with('schedule_event', 'e')
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('where')
    ->with('e.date >= :date_start')
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('andWhere')
    ->with('e.date < :date_end')
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('andWhere')
    ->with('e.channel = :channel')
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('andWhere')
    ->with('e.movie_id IS NOT NULL')
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('groupBy')
    ->with('date')
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('setParameter')
    ->with(':date_start', $date_start->format('Y-m-d'))
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('setParameter')
    ->with(':date_end', $date_end->format('Y-m-d'))
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i++))
    ->method('setParameter')
    ->with(':channel', $channel)
    ->will($this->returnSelf());
$builder
    ->expects($this->at($i))
    ->method('execute')
    ->will($this->returnValue($sth));

$exp = $this->getMockBuilder(ExpressionBuilder::class)
    ->disableOriginalConstructor()
    ->getMock();
$exp
    ->expects($this->once())
    ->method('isNotNull')
    ->with('e.movie_id')
    ->will($this->returnValue('e.movie_id IS NOT NULL'));

$conn = $this->getMockBuilder(Connection::class)
    ->disableOriginalConstructor()
    ->getMock();
$conn
    ->expects($this->once())
    ->method('createQueryBuilder')
    ->will($this->returnValue($builder));
$conn
    ->expects($this->once())
    ->method('getExpressionBuilder')
    ->will($this->returnValue($exp));

$em = $this->getMockBuilder(EntityManager::class)
    ->disableOriginalConstructor()
    ->getMock();
$em
    ->expects($this->once())
    ->method('getConnection')
    ->will($this->returnValue($conn));

$class = $this->getMockBuilder(ClassMetadata::class)
    ->disableOriginalConstructor()
    ->getMock();

$rep = new ScheduleEvent($em, $class); // EntityRepository

$this->assertEquals($result, $rep->getAllWeekEvents($day_start, $channel));


Тестируемый код
Это только треть всего метода. Там еще один такой запрос и обработка результата. Для примера я думаю и этого будет достаточно
function getAllWeekEvents(\DateTime $date_start, $channel)
{
    $date_end = clone $date_start;
    $date_end->modify('+7 day');

    return $this
        ->getEntityManager()
        ->getConnection()
        ->createQueryBuilder()
        ->select('e.date, COUNT(*) AS `total`')
        ->from('schedule_event', 'e')
        ->where('e.date >= :date_start')
        ->andWhere('e.date < :date_end')
        ->andWhere('e.channel = :channel')
        ->andWhere($this->getEntityManager()->getConnection()->getExpressionBuilder()->isNotNull('e.movie_id'))
        ->groupBy('date')
        ->setParameter(':date_start', $date_start->format('Y-m-d'))
        ->setParameter(':date_end', $date_end->format('Y-m-d'))
        ->setParameter(':channel', $channel)
        ->execute()
        ->fetchAll();
}

Я просто приведу пример простого на вид кода и его теста, возможно на их примере будет понятно что написать тест до реализации может быть не так уж и просто.

Во-первых, это узнаваемый DAL, который не факт, что надо тестировать.
Во-вторых, такие вещи лучше всего тестировать с помощью функционально эквивалентного стаба — т.е., у вас есть большая коллекция событий, есть поверх нее in-memory query engine, а потом вы просто сверяете, что ваш метод вернул именно то, что вы ожидаете.
Sign up to leave a comment.