Real-life unit tests

  • Tutorial
Часто мне приходилось слышать, что кто-то послушал лекцию или прочитал статью про юнит-тесты, вроде как всё понял; решил сам попробовать — и ничего не получилось.

Почему так получается?

По-видимому, причина в том, что юнит-тесты обычно демонстрируют на простых примерах. А в жизни код сложнее. В реальных проектах код использует базы данных, веб-сервисы, код, написанный другими компандами и т.д.

В этом видео на живом примере показано, как писать юнит-тесты для кода с внешними зависимостями.

www.devclub.eu/2011/06/06/asolntsev-real-life-unit-tests





Слайды можно найти здесь.

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

Похожие публикации

Комментарии 50

    +3
    решил сам попробовать — и ничего не получилось.
    Начинал использовать TDD несколько раз. Не получалось из-за нетривальных способов тестирования тривиальных методов. Простой пример: использование рефлексии, чтобы протестировать геттер/сеттер. Да ещё непонятно что писать сначала — тест на геттер (установка приватного свойства через рефлексию), геттер, тест на сеттер (проверка через геттер или через рефлексию?), сеттер или сначала тест на сеттер (проверка через рефлексию), сеттер, тест на геттер (установка через сеттер или рефлексию?), геттер.
      0
      P.S. Видео не работает :(
        0
        По ссылке работает. А в посте почему-то не работает, да. :(
          0
          Даже на vimeo.com/24502413 не работает :(
            0
            У меня на vimeo работает. Проверьте свой flash player.
        0
        Понятно.
        Да не надо их тестировать отдельно. Слишком много ненужной работы.
        Тестировать надо места в коде, в которых есть мало-мальски сложная логика или которые требуют объяснений.
          0
          Uncle Bob с вами категорически не согласен.
          cleancoder.posterous.com/100-test-coverage

            +5
            Вы не путайте, он говорит о том, что надо добиваться 100% покрытиями кода, а не о том, что геттеры/сеттеры надо отдельно тестировать. Геттеры/сеттеры должны тестироваться как часть других тестов. В частности, функциональных или интеграционных тестов. Например, если у тебя есть функциональный тест, который проверяет, что на экране появляется буква А, это автоматически означает, что соответствующий геттер протестирован. Юнит-тесты (да ещё и с использование рефлекшина) тут явно не нужен.
              0
              Дядюшка жжот :DD
              +2
              Так тоже пробовал, но сбивается ритм TDD, когда в геттеры/сеттеры нужно вносить логику сложнее чем присваивание/разыменование приватного свойства. Собственно обхожусь сейчас сначала одним тестом на геттеры/сеттеры типа
              function XXXGetterAndSetterOfValueTest() {
                $testValue = 123;
                $this->object->setValue($testValue);
                $this->assertEqual($object->getValue(), $testValue);
              
              , а потом, когда логика становится сложнее, начинаю использовать рефлексию и т. п.
              0
              А какая разница что писать сначало?
                +1
                В том-то и дело, что никакой. Яйцо и курица, с чего начинать не знаешь.
                +2
                > Простой пример: использование рефлексии, чтобы протестировать геттер/сеттер. Да ещё непонятно что писать сначала — тест на геттер (установка приватного свойства через рефлексию), геттер, тест на сеттер (проверка через геттер или через рефлексию?), сеттер или сначала тест на сеттер (проверка через рефлексию), сеттер, тест на геттер (установка через сеттер или рефлексию?), геттер.

                Зачем же такие извраты.
                Ровно 1 тест:
                1. Через сеттер ставим значение.
                2. Если не поставилось — фейлим тест.
                3. Через геттер берем поставленное значение и сравниваем с тем что ставили.

                Еще рекомендую почитать “The Art of Unit Testing". Тесты (юнит- и интеграционные) вообще сильно облегчают жизнь, особенно при разработке крупных проектов.
                  0
                  >2. Если не поставилось — фейлим тест.

                  Как мы это узнаём?
                    0
                    > Как мы это узнаём?

                    Т.к. я не знаю как у вас там все устроено и работает, то сказать ничего не могу по этому поводу.
                    Например, сеттер может выкинуть исключение если что-то пойдет не так (кривые входные данные например). Или, если сеттер вызывается на клиентской части, сериализует данные и засылает их на сервер, то можно смотреть что приходит на сервер после вызова сеттера. Ну и тому подобное.
                      +1
                      Не, банальный код вроде
                      class A {
                        private $value;
                      
                        function setValue($value) {
                          $this->value = $value;
                        }
                      
                        function getValue() {
                          return $this->value;
                        }
                      }

                      Эти методы можно тестировать либо с помощью рефлексии, что неудобно, если тестовый фреймворк её не поддерживает и нарушает инкапсуляцию, либо оба метода сразу, что несколько отходит, имхо, от модульных тестов и лежит ближе к интеграционным.
                        +2
                        Эмм… может я чего не понимаю, но зачем для такого кода тесты?
                          0
                          Ну я встречал пустые сеттеры. Заглушку ф-ии сделали (может даже автоматически), а написать присваивание забыли.
                            0
                            > Заглушку ф-ии сделали (может даже автоматически), а написать присваивание забыли.

                            За такое руки отрывать нужно :(
                            Для простого и быстрого тестирования в таком случае в голову приходит тот же самый вариант что я озвучил ранее — последовательный вызов сеттера и геттера без промежуточной проверки. В этом случае сразу будет понятно есть ли в этом классе проблемы с геттером или сеттером.
                            0
                            Хотя бы подстраховка от опечаток, чтобы пользователь в один прекрасный день не увидел 500-ю ошибку, а в логах не появилось что-то вроде «function undefined»
                            0
                            Или создавать ещё один конструктор, который во все внутренние переменные вставляет то, что ему сказано.
                            Соответственно, заполняем все внутренности моками, которые знают, чего ждать от сеттера, и тестируем.
                            Потом заставляем моки возвращать чего надо, и тестируем геттеры.
                              0
                              >либо оба метода сразу, что несколько отходит, имхо, от модульных тестов и лежит ближе к интеграционным.
                              А вы хотите добиться идеальных юнит тестов? Типа идеальный газ, абсолютный вакуум, да? Если задача решается, то какая разница отходите вы от философии «идеальных модульных тестов» или нет. Учитывая простоту геттеров-сеттеров, то нет ничего плохого в тестировании обоих сразу.
                                0
                                Как всякий неофит стремлюсь к идеальности :)
                                0
                                это потому и называется модульными тестами, а не тестами методов, что тестируется взаимодействие модуля с внешним миром, а не каждого его метода в отдельности.
                                  0
                                  Взаимодействие с внешним миром вроде как тестируется в интеграционных тестах, а в модульных тестируются внутренняя реализация.
                                    +1
                                    Нет, не совсем так. Интеграционные тесты предназначены для тестирования взаимодействия модулей и групп модулей на предмет соответствия результатов работы ТЗ. Юнит тестирование же предназначено для проверки работы программных модулей.
                                    Небольшой пример. Есть класс Profile.
                                    Юнит-тестами проверяется результат работы методов класса с различными входными параметрами (корректными и некорректными):

                                    setProfile()
                                    getProfile()
                                    createProfle()
                                    setAddress()
                                    etc.

                                    Интеграционными тестами проверяется:

                                    — Корректность пересчета стоимости доставки после смены адреса через setAddress(),
                                    — Корректное заполнение полей e-mail'а, высылаемого при регистрации (т.е. вызова createProfile())
                                    — etc

                                    Взаимодействие с внешним миром — это, скорее, относится к функциональному тестированию.
                          +1
                          Уходите от тестирования каждого чиха! Тестируйте работу программы, а не функции. Пользователю по факту фиолетово, работает ваш сеттер или нет, ему нужна работа приложения в целом. Раз у вас есть геттер/сеттер, значит он где-то используется — тестируйте этот кусок, скармливая ему различные данные. Ну, а если уж понадобилось проверить именно геттер/сеттер, то метож описан выше: скормили сеттеру данные, прочитали результат геттера, сравнили.
                            –1
                            Это отступает о принципов TDD — «ни строчки кода, не получив красную линию в тесте».
                              +1
                              В чем же? Вы пишите тест, проверяющий работу, предположим, кнопки, тест проваливается, т.к. для этой кнопки не написано никаких действий (а то и кнопки-то нет), вы пишите код, в т.ч. те самые геттер и сеттер. Все в рамках TDD. Если же эта кнопка работала и без геттера/сеттера, а вы их решили к работе этой кнопки прикрутить, то тут 2 варианта: 1) если ничего не изменится, то зачем вы вообще это делаете? 2) если начнет работать лучше, или код станет красивее, то это оптимизация/рефракторинг, которые по TDD можно проводить и без дополнительных тестов.
                              Ну и, в конце концов, нужно самому знать рамки. Эдак, руководствуясь вашей логикой, нужно действительно описывать каждую строчку, что долго и глупо. В народе говорят: «Заставь дурака богу молиться, он и лоб расшибет».

                              Думаю, стоит таки накидать статью о моем видении TDD (а точнее BDD), с которым я работаю.
                            +1
                            Вы хотите просто писать модульные тесты, или практиковать TDD? Если последнее, алгоритм простой:
                            1. Придумываете фичу.
                            2. Создаете тест — красная полоска.
                            3. Пишете минимум кода, чтоб тест прошел.
                            4. Рефакторинг.

                            Сначала тест — потом код, как следствие, нет сломанного теста — нет нового кода. И, конечно же, нет требования — нет теста :)

                            В вашем случае: пишете testSetSomeValue, допустите, что в случае успешной установки значения сеттер должен вернуть истину. Реализуйте метод setSomeValue { return true }, тест пройдет. Пишите тест testGetSomeValue. Т.к. в нем используется сеттер, последний придется изменить, ровно настолько, чтоб прошел новый тест. В итоге у вас два теста, оба нужные т.к. описывают поведение тестируемых методов: сеттер должен вернуть истину после установки значения, значение должно быть доступно через геттер после установки сеттером.

                            Рекомендую попробовать несколько принципов, в том числе из BDD, которые существенно упрощают написание тестов до кода:
                            1. Should вместо test — название теста отражает поведение.
                            2. Один тест — один ассерт.
                            3. AAA (Arrange — Act — Assert).

                            P.S. В комментариях обсуждается все что угодно, но только не TDD :)
                              0
                              Хочу практиковать TDD, но сталкиваюсь с невозможностью (вернее с непростой возможностью) декларировать поведение геттеров/сеттеров на уровне тестов и тестируемого объекта. Тупо не могу написать код геттера (из одной строки), пока не прошёл тест на сеттер (тоже из одной строки).

                              Допущения типа «в случае успешной установки значения сеттер должен вернуть истину», имхо, неприемлемы. Блин, из-за таких допущений формируется 42,37% претензий к моему любимому языку.
                                0
                                Не делайте допущений :)

                                Каким образом в реальном коде вы планируете узнать, что метод set отработал без ошибок? Уж явно не будите вызывать get и проверять, что значение установлено :) Вот определитесь с поведением и опишите его в тесте. Не нравится булево значение, смотрите, чтоб не было эксепшена, например.
                            0
                            Mockito жжет адским напалмом. Сколько раз я отказывался от тестирования из-за внешних зависимостей. Ух, теперь развернусь!
                              +2
                              все только и делают, что говорят о модульных тестах. а кто-нибудь знает как писать интеграционные? ага, интеграционные сложнее, поэтому их писать не интересно. а вот модульные примитивные, поэтому будем писать их %-)

                              лично я не разделяю тесты на модульные и интеграционные. есть просто тесты — они тестируют заданный функционал. и разработка строится по принципу: модуль1, для него тесты1, модуль2 зависит от модуль1, тесты2 строятся с предположением, что модуль1 протестирован и работает корректно. и даже если оба модуля некорректны, но тесты их тандем проходит, то и пофиг ;-)

                                0
                                Да, писать интеграционные тесты сложнее. Не только писать, но ещё и поддерживать и запускать. Да, там больше рутины, поэтому действительно не так интересно.

                                Только почему же об этом не пишут? Очень даже пишут.

                                А разделать их надо обязательно. Хотя бы потому, что модульные тесты должны запускаться автоматически при сбороке проекта, а интеграционные запускаются отдельно, ибо они гораздо медленнее и требуют поддержки.
                                  +2
                                  ну, это ты сейчас необоснованно экстраполируешь специфику своего проекта на тестирование вообще. например, скриптовые проекты могут не иметь этапа «сборки» или иметь её налету. да даже и в случае с «собираемыми» проектами — вовсе не обязательно перетестировать всё — достаточно провести только те тесты, которые касаются функциональности, которая изменилась. а модульные тесты точно также могут сильно тормозить, если там реализован какой-то сложный алгоритм. далее, понятие модуля — весьма условное. есть у меня допустим модуль логирования. прокрыт модульными тестами, всё как полагается. но со временем он так разросся, что было решено разбить его на подмодули. лёгкое движение рук и некогда православный модульный тест внезапно превратился в интеграционный, ибо тестирует уже взаимодействие нескольких модулей между собой. чисто модульное тестирование — это нечто похожее на тестирование каждой строчки кода независимо от остальных. вроде бы и 100% покрытие легко обеспечить, а всё-равно ошибки как-то просачиваются.
                                  –1
                                  > модуль2 зависит от модуль1

                                  При разработке через TDD именно таких моментов избегают. Юнит тесты узконаправленны и проверяют отдельную реализацию, чтобы только что написанный код соответствовал требованиям.
                                  Как правило, такие тесты запускаются «кусочками», т.е. для модулей и должны проходить быстро. А интеграционные обычно длятся долго и запускаются гораздо реже. (и о них тоже пишут, и не мало)

                                  > и даже если оба модуля некорректны, но тесты их тандем проходит

                                  Это уже вопрос о корректности написанных тестов
                                    +1
                                    Интеграционные/функциональные/приемочные (я так и не понял это одно и то же или нет) имеют кроме большого размера ещё одну неприятную особенность — не дают никакую информацию о том, где произошла ошибка, если результаты не соответствуют ожидаемым. При веб-разработке с MVC использую их как тесты контроллеров, когда модели и шаблоны оттестированы и если ошибка произошла, то с очень большой вероятностью именно в контроллере, который, собственно, и отвечает за интеграцию в этом паттерне.
                                      0
                                      Интеграционные — проверяют интеграцию компонентов, то есть она запускают ВСЕ компоненты системы и тестируют, что записи появятся в базе данных, сообщения попадут в очередь, файлы появятся на диске и т.д.

                                      Функциональные = приёмочные = UI Tests — проверяют, что для пользователя система выглядит так, как он ожидает. То есть если он три раза введёт неправильный пароль, на экране должна появиться надпись «пошёл вон!». А изменится ли что-нибудь при этом в базе, эти тесты не обязательно должны тестировать. То есть эти тесты зачастую эмулируют часть системы (например, веб-сервисы или базу) и поэтому проще в поддержке и быстрее, чем интеграционные тесты.
                                        0
                                        Но ведь при функциональных также запускаются все компоненты (необходимые для тестирования данного юз-кэйса), а значит разница собственно в том проверяем только возвращаемое значение или же анализируем и изменение окужения? То есть функциональные являются подмножеством интеграционных?
                                          0
                                          Не обязательно все.
                                          Например, мы обычно эмулируем веб-сервисы. Так и тестировать проще: если веб-сервис кинул ошибку, на экране должна появиться красная надпись. Если вернул «ок», на экране зелёная надпись.
                                          0
                                          > А изменится ли что-нибудь при этом в базе, эти тесты не обязательно должны тестировать.

                                          Я бы даже сказал, что они обязательно НЕ должны это тестировать :) И тесты скорее эмулируют не систему, а поведение пользователя системы, ожидая на производимые действия описанных в требованиях реакций.
                                            0
                                            Вы меня совсем запутали… Не понимаю с какого перепугу тесты должны эмулировать систему? Основное назначение тестов, имхо, эмулировать внешние воздействия к системе и проанализировать их соотвествиее ожидаемым результатам. А «эмулировать систему» должна сама система, ведь мы же её тестируем? Или я чего-то не понимаю в теории конечных автоматов и прочих тьюрингов, которых нам не давали?
                                              0
                                              Это мне ответ? Я же написал: «тесты скорее эмулируют не систему, а поведение пользователя системы».
                                          0
                                          Контроллеры, как и любой другой класс, отлично «тестируются» модульными тестами. Функциональные тесты — это тестирование черного ящика, т.е. вы взаимодействуете с системой как ее конечный пользователь и ассерты должны быть соответствующие.
                                        0
                                        Извините, не впечатлило. Ну и как обычно оставлю ссылку: martinfowler.com/articles/mocksArentStubs.html
                                          0
                                          Спасибо за видео. Я в своё время тоже записал подобный подкаст http://blog.byndyu.ru/2010/02/tdd_18.html
                                            +1
                                            на самом деле жаль, что вы не успели рассказали про очень про то, что перед написанием теста не нужно, а скорее всего придётся рефакторить, отоучая себя от таких вещей, как:

                                            1) Отсутствие интерфейсов и как результат нарушения инверсии зависимости. Когда у вас просто не получится замокать используемый класс в тестируемом методе. А он будет очень большим.
                                            2) использование статических паблик метдов в классе с логикой, а не в спец. хелпере или лучше вообще без них. Как например работа с DateTime.Now без интерфейса.
                                            3) Методы длиннее 10 строк и как результат классы делают всё. Опять же нарушение инверсии зависимости.

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

                                            Вообще на мой взгляд противники мок тестов возможно не разобрались с двумя вещами:
                                            1) Тестируем только 1 класс и только ЕГО методы, без БД, сервисов и т.д. Почему-то многие думаю, что без базы не смогут проверить бизнес-логику. Это не так. Нужно просто написать по 1 интеграционному тесту к базе с минимальным наборов параметров, чтобы убедиться, что результат будет таким, каким мы его ожидаем. Но это интеграционный тест.

                                            2)Сперва уже написали много логики, начинаем тестировать, а у в проекте оказывается говнокод, костыли и все перечисленные выше ошибки. И тут сразу обнаруживаются те, кто не прочь писать код по 100 строк в 1 методе и без комментариев. Сами себя обнаруживают. Конечно рефакторить не хочется, переделывать не хочется (а джунеоры и не знают что так нельзя). А протестировать такой код невозомжно. Просто невозможно. А уж поддерживать такие тесты тем более будет нетривиальная задача.

                                            В итоге люди просто отказываются, в сторону накликивания всего и вся на интерфейсе, нарушая при этом главное правило тестирования (и написания кода метода) — проверять аргументы и данные(например те, что из БД придут) на граничные и недопустимые значения. Это просто невозможно успеть сделать не используя юнит-тесты и моки. По времени не успеть.

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

                                              0
                                              Зачетный комментарий. По делу парень говорит.
                                              Не понятно за что его минуснули.

                                              Добавлю ещё пару строк от себя:
                                              тесты — это лакмусовая бумажка гибкой и правильной архитектуры приложения. Если ты не можешь свой код протестировать, значит у тебя плохо продумана структура классов и интерфейсов и есть нужна рефакторить, пока не поздно.
                                              Даже есть такой прием, пишешь код и продумываешь, как бы ты его протестировал, при этом сам тест уже не пишешь, но твой код лучше. Хотя ИХМО 100% покрытие логики очень спасает в сложных ситуациях.

                                              Если вы все-таки натянули тест на свой код, и он у вас при каждом изменении ломается, то это тоже сигнал про то, что у вас в коде не всё в порядке. Это так называемые хрупкие тесты. Они написаны с учетом костылей, причем каждый новый костыль ломает предыдущие.

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

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

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое