Комментарии 90
напишите как тестировать не только простейшие примеры, в которые тестирования и так не надо, а сложные системы, где метод вызивает внутри себ много других, работает с базой, а выводит совсем не инт или булл, а генерирует вывод через шаблон к примеру. ну и принимает пользовательский ввод или зависит от других ранее вызванных методов, а еще от сессии и ее состони
Дело в том, что поводом к написанию данной статьи стал вопрос: «А что есть почитать по PHPUnit на русском?» Начал гуглить — и не нашел. Я прекрасно сознаю, что здесь изложены только основы — но, вы удивитесь, многие не знают и этого. Если тема интересна, то позже попробую описать глубже.
Весьма интересна, на работе как раз столкнулись с проблемой модификации кода, писало более двадцати людей, логика не везде понятна. Вводим юнит тестирование параллельно с рефакторингом. Посему очень хотелось бы почитать про mock объекты
wiki.agiledev.ru/doku.php?id=tdd — пожалуйста =)

пс: я конечно понимаю что постоянно появляются новые люди, которые всегда будут открывать что-то новое, но хотелось видеть статьи бы не старого нового, а инновационо нового =)
Спасибо :)

ПС благодаря этим новеньким процесс не стоит на месте ;)
Да, но на данный момент в ZendFramework присутствует класс Zend_Test. Вот как его использовать, не очень много информации
Я тут убедился, что даже в книжках, полностью посвещенных TDD, примеры высосаны из пальца (типа:«проверим, что тут 2х2=4»). В итоге, прочитав, так и не понял как это использовать в реальном проекте.
На «дважды два четыре» просто понять суть, а транслировать на другую задачу уже не должно бы составлять труда.
А что именно нужно проверить?
Как значение выглядит на этом самом шаблоне — это отдельная тема. А получаемое значение мы тестируем именно так, как написано в топике.
По поводу сложных систем — любая из сложных систем в конечном итоге состоит из минимальных простейших блоков. А тестировать нужно именно самые атомарные элементы алгоритмов.
А где остановиться? Получается, тестировать ничего не надо, самые простейшие операции (и, или, не и т.п.) уже давно оттестированы производителем процессора :)
Ну обычно это методы классов и интерфейсов. Ну нужно писать отдельно один «Большой Адронный Тест» на всю систему.
это уже принципиально другой уровень тестирования — функциональное. оно осуществляется другими инструментами.
Метод вызывает в себе другие. Все методы тестируются по-отдельности.

Например:
1) Есть Method1(), который выполняет некую операцию
2) Есть Method2(), который выполняя операцию, использует Method1()
3) Вы тестируете поведение Method1() по всем необходимым тест-кейсам (если все тесты OK, вы считаете, что метод работает как нужно)
4) Вы тестируете поведение Method2() — тесту все равно, что вы там использовали внутри, лишь бы результат совпадал. Здесь и есть один из ключевых моментов — вы тестируете именно поведение и результат Method2(), а не поведение Method1() в Method2().
Вот в этом все равно и заключается главная проблема. А если метод лезет в базу, или открывает соединение по сокету, в качестве сервера… Юнит тестами отлично покрываются методы, которые возращают результат умножения или выводят на экран «Hello world». Или писать огромную кучу моков, заглушек, и непонять еще чего, или позволять Юнит-тесту работать с живой базой или реальными сокетами.
Мы в работе использовали подход тестирования поведения (behavior testing) и использовали mock-и. Тесты (иногда дублирующие), которые тестировали состояния базы, соединения и так далее — использовались у нас на стадии интеграционного тестирования (т.е. тестирование не собственно логики, а тестирование именно по сути отправки запросов, соединений и так далее).

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

> «Юнит тестами отлично покрываются методы, которые возращают результат умножения или выводят на экран «Hello world».»
Юнит-тестами отлично покрываются любые методы, которые возвращают определенный, ожидаемый и измеряемый результат.
Делали мы как-то тесты с живой базой. Получилась весьма отвратно — тесты работали медленно и содержали кучу кода: setup базы, cleanup базы — тонны SQL запросов, невозможность параллельного тестирования.

Вывод — делать моки. Это позволит не только избавиться от зависимости от БД, но и, например, автоматизировать валидацию поведения при лежащем сервере БД или битых данных (inconsistent data).

Хорошая новость в том, что заполучив единожды мок на БД или сокет в следующем проекте его не придется делать с нуля. Тут, безусловно, возможны проблемы с дизайном кода и вопросы типа «как подсунуть мок внутрь логики?» — но это отдельная тема, TDD в помощь.
Плохая новость в том, что большинство решения для работы с БД, слабо поддаются мокингу, :) поскольку сами созданы без использование этих принципов. Некоторые современные технологии для работы с БД(ORM), приближают нас к этому. Но пока все еще в стадии развития, хотя самому TDD уже предостаточно времени.
Скажем так. TDD выходит из стадии испытания и начинает использоваться в индустрии(asp net mvc полностью построен на нем)
> Плохая новость в том, что большинство решения для работы с БД, слабо поддаются мокингу.

Тю, кто мешает написать враппер вокруг решения для работы с БД и использовать его в проекте? ;-)
Ни кто не мешает :) Чаще всего так делается. Создаем свой интерфейс, реализуем обертку вокруг какого нить Database и вперед :) Но я же не сказал, что это не возможно :)
Мне кажется, этот вопрос немного не по адресу. Тестировать пользовательский ввод, зависимости от других ранее вызванных методов, а еще от сессии и ее состояния — это скорее задача функционального (приемочного) тестирования. Для функционального тестирования веб-приложений есть известный инструмент — Selenium (плагин для ФФ), других лично я не знаю, но, думаю, они существуют и вы их сможете без труда найти.

Задача модульного тестирования, как следует из названия — тестировать модули. Модуль — это класс, библиотека функций и т. д. Классическое модульное тестирование — тестирование черного ящика, то есть что там делается внутри метода — не важно (напротив, даже важно именно не знать этого; полагаться на особенности реализации тестируемого модуля — один из неприятных запахов тестового кода). В этой связи также становится неясен вопрос о «сложных системах, где метод вызивает внутри себ много других». Пусть вызывает. Протестируй его.

Единственный вопрос, с которым соглашусь — это работа с базой. Тестирование работы с БД — отдельная песня, основной лейт-мотив которой — тестируй на реальной базе. Нет, не на продакшн-базе :) Просто воссоздай в тестовом окружении ровно то, что есть в реальном проекте, только без данных (или с их необходимым минимумом). Все данные, нужные тесту, он должен впихнуть в базу сам, создавая фикстуру.
<?php
print (int)((0.1 + 0.7) * 10);
// думаете 8? проверьте…
// или лучше так: «Вы еще не используете BC Math? Тогда мы идем к вам!»

Жесть пример. Причем самое интересное что тот же результат выдает и C. Убиться
Простите если я тупой.
А зачем там int?
Без int выводит 8
без int получается число, которое чуть-чуть меньше 8, но при выводе выводится как 8.
Проверить легко: print ((0.1 + 0.7) * 10) — 8;
Лично у меня в ветке 3.3 при количестве тестов >700 очень сильно текла память. Копаться в изменениях между 3.2 и 3.3 не стал, поэтому откатился до 3.2.
можете рассказать о своем опыте использовании тестов? о том что нужно тестировать обязательно, а что не стоит, сколько в среднем занимает написание одного теста, и как часто тесты помогают отловить ошибки?
Чем больше тестов — тем больше шансов, что сломать все будет сложно. Написание тестов также позволяет найти места, где логика вашего приложения выстроена неправильно. Если вы не можете написать тест на какой-то метод/функция — значит вы неправильно спланировали интерфейсы и внутренние функции.

Можно использовать mockи для параметров функции, чтобы не создавать какие-то сложные объекты.

К сожалению тесты не всегда помогают отловить ошибки, но это лишь значит что не было написано соответствующего теста ;) Как только такая ситуация возникает надо сразу написать тест и поправить ошибку.

В среднем написание теста занимает минут 10-15 для простых функций.
А что есть почитать по PHPUnit на русском?


Я нашел довольно хорошо написанный материал по Юнит-тестированию в книге «Профессиональное программирование на PHP» от Джорджа Шлосснейгла.

В сети её можно скачать в PDF ;)
всем кому интересно и не читал еще, рекомендую почитать «Профессиональное программирование на PHP (Джордж Шлоснейгл)». Там блочное тестирование освещено достаточно подробно, впрочем как и все остальное.
Да, отличная книга! Уберегла от множества ошибок в проектировании.
Даа, это целая отдельная тема в TDD — mock-и и разница между тестированием состояния и поведения (про Expectations Testing и все такое)
доступно написано. Все сам хочу использовать (пишу на cakephp ) но клиенты никак немогут понять почему я буду делать таски дольше и они будут платить больше.
Так TDD и не должно создавать ситуацию «дольше и платить больше». Это просто альтернативный (я убежден — что более надежный) способ создавать ПО.

P. S. CakePHP + Unit-тестирование = Sehr gut!
да согласен, но он увеличивает время разработки и уменьшает время на потдержку. Кроме того, бытует мнение что юнит тесты нужно применять только там где они нужны.
В общем при сдоровой конкуренции (фриланс) в основном выигрывает тот, кто сделает быстрее (ну и качественней), но клиент конечно же в код не лезет и понятное дело, что ему чем быстрее, тем лучше. И объясняй ему что такое юнит тесты и что это ему даст.
Скажу чисто по опыту, никакой теории.

1. Действительно, чтобы стать крутым TDD-разработчиком, нужно время для «въезжания» в тему, но потом продуктивность вырастет. Вы не тратите время на обдумывание кода (все поведение обдумывается на этапе написания тестов, код пишется только, чтобы их выполнить). И что ооочень важно — в большинстве случаев никакого дебага, ибо ошибки ищутся и исправляются в разы быстрее.
2. Объяснять заказчику, что это такое, не нужно ни в коем случае — зачем ему это. Он все равно не понимает, что как разрабатывается.
3. > «Кроме того, бытует мнение что юнит тесты нужно применять только там где они нужны.» В рамках одного проекта — или везде или нигде — частичное покрытие системы тестами — вот где обычно кроется краеугольный камень TDD — все потраченное на тесты время действительно оказывается потраченным зря. Часть модулей протестирована, но общее поведение системы протестировать невозможно.
4. TDD ведет к постоянной интеграции, а это очень круто :)
юнит тесты не увеличивают качество кода! Они просто позволяют его в будущем дешевле изменять
bishop-it.ru/?p=119

Например, Michael Feathers в своей книге Working Effectively with Legacy Code не призывает покрывать все 100% кода тестами. Мало того, он даже не призывает всегда покрывать тестами код, который вы собираетесь изменять. Он призывает думать. Каждый раз делать осознанный выбор — нужны ли вам в данном месте юнит тесты или нет. Всем советую купить и прочитать эту его книгу. Эта книга — лучшее описание теории и практики написания юнит тестов, что я встречал. Причем именно с точки зрения практики — большая часть книги посвящена описанию стандартных приемов работы со старым кодом. А сам автор имеет огромный практический опыт по улучшению legacy кода.
Качество кода зависит от разработчика прежде всего. Улучшение legacy-кода и переход уже существующего легаси-проекта на TDD и CI — это отдельная большая тема, но методы существуют.

Про покрытие — я, разумеется, условно рассматривал. 100%-го покрытия не бывает на практике. Здесь нужно чувствовать определенную грань. Не нужно плодить лишние и дублирующие тест-кейсы.

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


Есть путь менее правильный, но более быстрый — тестирование осуществляется вручную с консоли, а потом результаты просто копи-пастятся в доктест. Это не даёт гибкости в том плане, что удобны лишь проверки на полное равенство вывода эталонному, но зато накладных расходов на написание таких тестов нет. Поясню примером:

Захотелось мне написать функцию для разложения числа на простые множители. Создаю файлик my.py со следующим кодом:

def f(x):
        assert x > 1
        i = 2
        while i <= x:
                if x % i == 0:
                        x /= i
                        yield i
                else:
                        i += 1


Как проще и быстрее тестировать такое? Правильно, в консоли. Запускаю с тестовыми параметрами и смотрю результат:

>>> from my import f
>>> list(f(1))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "my.py", line 8, in f
    assert x > 1
AssertionError
>>> list(f(3))
[3]
>>> list(f(4))
[2, 2]
>>> list(f(60))
[2, 2, 3, 5]


Визуально понимаю, что результат меня устраивает. Использую буфер обмена для помещения вывода в код как теста:

def f(x):
        '''
        >>> list(f(1))
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
          File "my.py", line 8, in f
            assert x > 1
        AssertionError
        >>> list(f(3))
        [3]
        >>> list(f(4))
        [2, 2]
        >>> list(f(60))
        [2, 2, 3, 5]
        >>>
        '''
        assert x > 1
        i = 2
        while i <= x:
                if x % i == 0:
                        x /= i
                        yield i
                else:
                        i += 1


Теперь как-то надо уметь этот тест запускать. Простейший способ — добавить в конец файла немножко кода для вызова теста текущего модуля при загрузке этого модуля первым:

if __name__ == '__main__':
        import doctest
        doctest.testmod()


Запускаем:
$ python my.py
$


Вывод пустой — всё в порядке.

Сэмулируем теперь ошибку логики, поправив тест:
$ python my.py
*******************
File "my.py", line 13, in __main__.f
Failed example:
    list(f(60))
Expected:
    [2, 2, 3, 6]
Got:
    [2, 2, 3, 5]
*******************
1 items had failures:
   1 of   4 in __main__.f
***Test Failed*** 1 failures.


Кто-то мог заметить, что в трэйсбэке исключения зашит номер строки — это мелочи, фрэймворк корректно обрабатывает их отличия от эталона в тесте.

Итого — тесты имеются, а накладных расходов — минимум.

В рамках одного проекта — или везде или нигде


Не соглашусь. В рамках большого проекта начать писать тесты на всё сразу — не хватит сил, не писать по-прежнему — неразумно, потому тесты пишутся на новый, изменившийся функционал и на обнаруженные ошибки. Через некоторое время можно не переживать о том, что значительная часть кода не покрыта тестами — значит, этот код относительно стабилен.
«Визуально понимаю, что результат меня устраивает.»
Ну так вы, по сути, тест и написали. Только одно из преимуществ тестов — запустить их все сразу, провести постоянную интеграцию. Не каждый же раз вручную запускать каждый кусочек, который нужно проверить. Впрочем, может я что-то не так понял?

«В рамках большого проекта начать писать тесты на всё сразу — не хватит сил, не писать по-прежнему — неразумно, потому тесты пишутся на новый, изменившийся функционал и на обнаруженные ошибки. Через некоторое время можно не переживать о том, что значительная часть кода не покрыта тестами — значит, этот код относительно стабилен.»
Естественно. На все сразу и смысла нет. То, что вы написали — это нормальная стратегия внедрения TDD в проект legacy-системы. Именно так это и делается.

А насчет того, как и где проще всего тестировать — как по мне, так прямо в IDE, что называется «не отходя от кассы». Мы для этой цели с Visual Studio использовали TestDriven.NET
Для других платформ и технологий тоже есть что-то подобное (для Java точно).
Не каждый же раз вручную запускать каждый кусочек, который нужно проверить. Впрочем, может я что-то не так понял?


Угу, это в примере было проще запустить тестирование именно так. А вообще доктесты (а именно так называются тесты из примера) прекрасно интегрируются в классические юнит-тесты. Сказал про минус доктестов, но забыл сказать про главный плюс — они являются ещё и документацией к функции, потому так и называются.

проще всего тестировать — как по мне, так прямо в IDE


С трудом понимаю… В смысле в IDE есть интерфейс для запуска конкретного теста, группы тестов? Или для создания тестов? Опишите, пожалуйста.
TDD — для команд, для постоянной разработки, когда задача решается не в течении недели двух и благополучно забывается.

Уммм, представил себе идеальный мир, где все используют TDD и Agile.

К тебе обращаются как к разработчику-фрилансеру и просят решить задачу в рамках проекта.
Ты подключаешься к svn(или любой другой системе контроля версии), получаешь последнею версию, просмотрел набор тестов(По названию тестов, быстро понял где что проверяется :)).
Написал тесты для своей задачи, реализовал их, немного по-рефакторил, запустил всю сборку на тестирование.
Сделал commit, где то там у заказчика сработала CI, все успешно собралось и протестировалось.
Пошел забрал свои деньги в банке :)
Не только для команд. Один человек вполне может это использовать, как и команда.
Просто для команд это куда более критично, когда над одним и тем же кодом работает больше одного человека.
Западники уже реализуют подобные механизмы. Collaborative Development Environments. У нас пока это все только из области фантастики. Хотя технически — вообще все легко.
Ну это больше организационный вопрос, нежели технический — когда будут понимающие проджект-манагеры, они введут эти штуки в рабочий режим.
Что именно проверять в тесте, что мокать, что не мокать, как поступать с приватными методами, как быть при работе с БД. Нет смысла(просто не получится) раскрывать все эти вопросы в одной статье, которая тем более привязана к какой-то технологии(языку программирования). Надо рассматривать их отдельно и пробовать, пробовать, пробовать. И Бек и Файлер пишут, и не только они, что любая команды которая начинает использовать TDD(и unit тестирование в рамках TDD), в среднем тратят по пол года, чтобы хоть что-то понять и наработать свои практики.
Подскажите какой-нибудь опенсорсный пхп-проект, где это все хорошо реализовано.
этот проект называется PHPUnit, в каталоге Tests дистрибутива все и увидите
Я думаю, товарищ ТМС имел в виду проект, сделанный с использованием TDD и PHPUnit, где в исходных кодах можно найти не только сам код, но и наборы тестов.
Вроде Limb PHP framework полностью покрыт тестами
При использовании Симфонии тоже настоятельно рекомендуют в документации.
«полностью»
мало того, что абсолютно 100% покрытие невозможно, так это ещё и считается плохой практикой — писать тестов больше, чем нужно.
Может, для entrprise-level языков такая парадигма разработки и приемлема, но для ПХП — это просто выглядит кривлянием.
Время на написание этих тестов несоизмеримо больше, чем может понадобиться на гипотетический отлов сложно улавливаемых ошибок при внесении очередного изменения.

время на написание тестов несоизмеримо меньше времени, которое вы тратите на то же самое, но ручное тестирование в процессе написания кода.
хе, занятно: у комментария выше нет времени написания.
Тесты крайне прямолинейны и вероятность ошибки в них куда ниже, чем вероятность ошибиться в коде, держа все в голове во время написания.
>Поскольку PHPUnit уже стал стандартом де-факто в тестировании PHP приложений, то на нем и остановимся.
с чего принято такое решение?
репозиторий жив, баги фиксяться, пользоваться проще (имхо), гуя нормальная есть, нет зависимости от пакетов PEAR

по теме:
forum.agiledev.ru/index.php?t=msg&&th=1173&goto=7201#msg_7201
simpletest.svn.sourceforge.net/viewvc/simpletest/
Объясните, пожалуйста, кто разбирается.

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

По-простому пишется код, разбирающий csv и записывающий в базу. Делается тестовый файл с несколькими пограничными вариантами, загружаем его и смотрим во всех частях сайта, что получается.

А как с тестами? Я не могу даже охватить все сферы, которые в этой задаче нужно протестировать. Корректность вывода товаров в сокращенном и полном видах, работоспособность поиска, корректность введенных данных, корректность формата загружаемого файла, разные вопросы затирания старых данных, ошибки дублирования записей.

На такое пишут тесты? Где бы про это узнать, а то элементарных примеров много, но не понятно, как это масштабируется.
У вас есть задача разобрать CSV файл и поместить данные в БД. По сути любая задача разбивается на много маленьких.
Например в контексте этой задачи есть такая:
«Прочитать строку из файла, распарсить по правилам и разместить в массиве нужные значение из строки.»
Я пишу тест на входе метода строка на выходе массив и проверяю массив(actual и expected).
Далее с помощью тестов я описываю те ситуации которые должен обрабатывать мой метод. Например, если пропущено значение в строке или формат строки не соответствует ожиданиям. И так далее и тому подобное.
По сути тесты это проверка ситуаций, из которых состоит например модуль, который берет данные из файла и помещает из в БД.
Мне видится две проблемы:

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

2. Тестами можно проверить формальные вещи. Но если я захочу более интеллектуальный контроль, то придется думать над алгоритмами тестов. И здесь уже я о некоторых вариантах могу даже не догадываться. Скажем, захочу проверить безопасность веб-формы. Это ведь нужно очень разноплановые тесты писать. Ну и опять же это небыстрое дело.
Тесты покроют всю основную функциональность, их будет ровно столько сколько ситуаций обрабатывает твой код. Естественно если ты меняешь поведения кода, надо будет менять тесты. Вернее ты в начале подправишь тесты, а потом код. И еще при правильном написании тестов, если не меняется поведение метода, то не меняются и сами тесты.
Из практики.
Был большой метод который добавлял route, но в процессе добавления надо было проверить его уникальность, и это тоже было в методе(был большой, плохой метод :), там вообще было много не нужного).
Был написан тест(их там было несколько) который проверял наличия исключения при не уникальности маршрута(именно это и должен был делать наш метод).
Тестов которые бы проверяли работу по проверки уникальности не было(так как метод этого не должен делать)
В результате в момент рефакторинга все ненужные куски кода из метода, растащили по другим классам и методам, сама объявленная логика метода не поменялась, тесты так же не пришлось трогать.
А все потому что они были написаны правильно

Тесты должны быть простыми. Есть такое правильно. Если тест непонятен и требует слишком много, значит сам метод надо делать. он делает не свою работу. Тут тесты выступают в качестве инструмента, для построения архитектуры. На agiledev.ru есть очень много хороших статьей. Но опять повторюсь, надо пробовать и пробовать.
пользуюсь simple-tests, но теперь из статьи понимаю что надо наверно переходить на phpUnit. Например как в сипл-тестах реализовать конструкцию

… public function testPower($a, $b, $c)
{
$this->assertEquals($c, $this->fixture->power($a, $b));
}
public function providerPower()…

как я понимаю, метод providerPower реализует перебор нескольких вариантов вызова testPower. В симпл-тестах подобное приходится разрабатывать «на коленке». Или может я что-то упускаю?
НЛО прилетело и опубликовало эту надпись здесь
Спасибо за статью.
Именно то что я хотел прочитать, но до поиска руки не доходили; и о чудо оно само нашлось.
+1
Отличная статья. но есть один вопрос.
Понимаю, что примеры взяты из мануала к PHPUnit, но всё-таки можете объяснить почему

Для создания иерархии TestSuite-ов используется непонятный class SpecificTests и public static function suite(), вместо того, чтобы просто делать class SpecificTests extends PHPUnit_Framework_TestSuite и прописывать всё у него в конструкторе или в setUp()?
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.