Unit-тесты, пытаемся писать правильно, чтобы потом не было мучительно больно

Большинство людей не умеют писать unit-тесты. И даже те, кто применяет модульные тесты в ежедневной разработке, зачастую признают, что получившиеся тесты иногда не очень эффективны по определенным причинам. К этой категории людей я могу отнести и себя. В первую очередь, такой «причиной» является некоторая появляющаяся «инертность» кода, заключающаяся в том, что если требуется немного изменить какой-то ключевой алгоритм, добавить пару строчек кода, то при этом «падают» ~100 модульных тестов и приходится тратить продолжительное время на то чтобы заставить их работать вновь. Итак, приступим к «хорошим рекомендациям» при написании автоматических модульных тестов. Нет, я не буду капитаном очевидностью, в очередной раз описывая популярный стиль написания тестов под названием AAA (Arange-Act-Assert). Зато попытаюсь объяснить, чем отличается Mock от Stub-а и что далеко не все тестовые объекты — «моки».

Глобально модульные тесты можно условно поделить на две группы: тесты состояния (state based) и тесты взаимодействия (interaction tests).

Тесты состояния — тесты, проверяющие что вызываемый метод объекта отработал корректно, проверяя состояние тестируемого объекта после вызова метода.

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

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

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

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

Стоит заметить что существует классический труд по модульным тестам за авторством Жерарда Месароша под названием "xUnit test patterns: refactoring test code", в котором автор вводит аж 5 видов тестовых объектов, которые могут запросто запутать неподготовленного человека:

— dummy object, который обычно передается в тестируемый класс в качестве параметра, но не имеет поведения, с ним ничего не происходит, никакие методы не вызываются. Примером таких dummy-объектов являются new object(), null, «Ignored String» и т.д.

— test stub (заглушка), используется для получения данных из внешней зависимости, подменяя её. При этом игнорирует все данные, могущие поступать из тестируемого объекта в stub. Один из самых популярных видов тестовых объектов. Тестируемый объект использует чтение из конфигурационного файла? Передаем ему ConfigFileStub возвращающий тестовые строки конфигурации для избавления зависимости на файловую систему.

— test spy (тестовый шпион), используется для тестов взаимодействия, основной функцией является запись данных и вызовов, поступающих из тестируемого объекта для последующей проверки корректности вызова зависимого объекта. Позволяет проверить логику именно нашего тестируемого объекта, без проверок зависимых объектов.

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

— fake object (фальшивый объект), используется в основном чтобы запускать (незапускаемые) тесты (быстрее) и ускорения их работы. Эдакая замена тяжеловесного внешнего зависимого объекта его легковесной реализацией. Основные примеры — эмулятор для конкретного приложения БД в памяти (fake database) или фальшивый вебсервис.

Roy Osherove в книге "The Art of Unit Testing" предлагает упростить данную классификацию и оставляет всего три типа тестовых объектов — Fakes, Stubs и Mocks. Причем Fake может быть как stub-ом, так и mock-ом, а тестовый шпион становится mock-ом. Хотя лично мне не очень нравится перемешивание тестового шпиона и мок-объекта.

Запутанно? Возможно. У данной статьи была задача показать, что написание правильных модульных тестов — достаточно сложная задача, и что не всё то mock, что создаётся в посредством mock-frameworks.

Надеюсь, я сумел подвести читателя к основной мысли — написание качественных модульных тестов дело достаточно непростое. Модульные тесты подчиняются тем же правилам, что и основное приложение, которое они тестируют. При написании модульных тестов следует тестировать модули настолько изолированно друг от друга, насколько это возможно, и различные разновидности применяемых тестовых объектов в этом, безусловно, помогают. Стиль написания модульных тестов должен быть таким же, как если бы вы писали само приложения — без копипастов, с продуманными классами, поведением, логикой.
Share post

Similar posts

Comments 22

    +15
    Спасибо за статью, уяснил для себя разницу между терминами.
    Вот только заголовок не соответствует содержанию. Вы описали термины, и это хорошо, но не описали, как же «писать юнит-тесты правильно, чтобы потом не было мучительно больно». Если это первая статья из серии, то это стоит где-то указать.

    И вопрос по самой статье: не являются ли тесты взаимодействия зачастую интеграционными тестами? Ведь интеграция различных объектов наверняка включает в себя их взаимодействие, хотя, наверное, не любое взаимодействие можно назвать интеграцией.
    Возможно, разделение как-то можно связать с понятиями агрегации и композиции как типами ассоциации объектов?
      +2
      Ответил комментарием ниже, мышка соскользнула :)
      +1
      Основной мыслью статьи была мысль, что не стоит писать тесты «абы как». Что написание качественных тестов дело достаточно непростое и к коду с тестами стоит относиться также как и к коду основной системы. Как писать правильно, говорите… Как минимум, используя описанные тестовые объекты :) Я могу написать про AAA, рекомендации по именованию проектов и отдельных методов согласно рекомендациям того же Roy Osheron, если интересно. Просто думал что тема достаточно избита.

      Не совсем. Интеграционные тесты — это тесты, проверяющие работоспособность двух или более модулей системы, но в совокупности, то есть нескольких объектов как единого блока. В тестах взаимодействия тестируется всё-таки определенный объект, а вместо зависимостей используются тестовые объекты, такие как моки или тестовые шпионы.
      Например, есть класс, который при определенных условиях «дёргает» веб-сервис через зависимый объект. И нам надо проверить, что определенный метод зависимого объекта действительно вызывается. Логичным будет в качестве зависимого класса передать не реальный класс, работающий с вебсервисом (это будет интеграционное тестирование), и не заглушку, которую мы возможно заходим использовать при тестировании состояния, а тестовый шпион, и в конце теста проверить, что определенный метод зависимого объекта при требуемых условиях действительно был вызван (тест взаимодействия).
        +1
        Спасибо, это хорошее объяснение! Мне кажется, стоит добавить одно-два предложения в этом духе в абзац про возможное превращение юнит-теста в интеграционный.
          +1
          Добавил. И попутно немного переписал выводы. Рад что статья оказалась полезна!
            0
            круто-круто.

            а обзор провайдеров моков, стабов, упрощение жизни программиста? риномоки, моq, moles? или как, писать все эти шпиёны-заглушки ручками?
        0
        Я бы отметил также, что тесты состояния и взаимодействия (они же тесты поведения — behaviour testing) — обычно взаимоисключающие виды тестов, которыми можно тестировать один и тот же объект (Martin Fowler отлично расписывает разницу в своей статье Mocks aren't Stubs).

        И еще ремарка:
        «Стоит также заметить, что модульный (unit) тест может запросто превратиться в интеграционный тест, если при тестировании используется реальное окружение (внешние зависимости) — такие как база данных, файловая система и т.д.»
        Я бы не сказал, что что-то запросто во что-то превращается. Юнит-тест тестирует логику конкретного юнита, модуля. А интеграционный тест — именно взаимодействие. Подключив вместо мок-объектов реальные компоненты, мы не перестаем тестировать логику, и при этом не нужно рассчитывать, что взаимодействие протестируется «само по себе», на основе уже существующих тестов. Путаница возникает из за того, что интеграционные тесты обычно тоже _являются_ юнит тестами, в том смысле, что используется тот же тест-фреймворк, итд, но это другие тесты.

          +1
          Статью, безусловно, читал, но то что первое исключает второе, хоть убейте, не помню. Если несложно, приведите полную цитату.

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

          Похожи они разве что тем, что для их написания используются одинаковые фреймворки. «С технологической точки зрения интеграционное тестирование является количественным развитием модульного, поскольку так же, как и модульное тестирование, оперирует интерфейсами модулей и подсистем и требует создания тестового окружения, включая заглушки (Stub) на месте отсутствующих модулей. Основная разница между модульным и интеграционным тестированием состоит в целях, то есть в типах обнаруживаемых дефектов, которые, в свою очередь, определяют стратегию выбора входных данных и методов анализа.» Интуит
            0
            Нет, я не путаю :) Постоянно использую и то, и другое. Я как раз и зацепился за вашу фразу, что «моудльный тест может запросто превратиться в интеграционный, если при тестировании используется реальное окружеие».

            Я отметил именно то, что если подключить реальную систему вместо одного из fake-ов, тест не станет интеграционным. Он по-прежнему будет тестировать логику вашего кода, а не интеграцию с внешней системой, то есть не нужно надеяться на то, что подключив реальный модуль, мы «заодно» полностью протестируем интеграцию.

            Про разницу в подходах в статье Фаулера:

            Весь параграф «Choosing Between the Differences», начиная с «I'll begin with the state versus behavior verification choice». Ну, а также, весь параграф «So should I be a classicist or a mockist?». Он повсюду подразумевает разницу и необходимость выбора.

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

          Я думаю, хорошо бы, чтобы статья оставляла хоть немного позитивного впечатления: может, и сложно, но понятно, как. Может, какие-то примеры всё-таки?..
            0
            Значит наш с вами опыт, банально, различается. Потому что я знаю множество людей, которые пишут такие юнит тесты, которые хочется удалить и переписать заново.

            Какие примеры? Используя какой-либо конкретный xUnit фреймворк что ли?
            +12
            Может я чего-то упустил, но кратко статья состоит из следующего:
            1. Тесты сложная штука, потому что
            2. Есть много сложных терминов — вот они, причём каждый выдумывает себе свои варианты, от этого ещё сложнее!
            3. Надеюсь после таких сложных терминов вы понял, что тестирование это супер сложный процесс

            Не вижу тут никакой логики, мне начхать как всякие гуру называют фальшивые объекты которые я использую, от того что они вводят якобы сложные термины мне не чуть не сложнее создавать эти объекты
              +1
              Хех, согласен с вами — создавая мок или стаб, мне пофиг, как его называют — я даже не думаю об этом. Всмысле, вообще не думаю, просто использую и все. Знаю только, что это какой-то «фейковый» объект — остальное по барабану.
                –1
                Странно на самом деле так рассуждать.
                Может быть вы также не используете и шаблоны проектирования, потому что «начхать, всё итак работает»?
                  +1
                  Я бы не приводил столь категоричное сравнение — это немного другое. Я использую моки, стабы и прочие фейки, когда это необходимо, просто не думаю об этом — просто использую на автомате то, что нужно.

                  Хотя… может вы и правы — если кому-то об этом рассказывать, надо же называть вещи своими (разными) именами.
                    +1
                    1. Называть нужно, но если даже гуру не пришли к единой терминологии, то можно обойтись и без терминов, сравнение с паттернами тут не уместно — они реально разные и названия у них устояшиеся, а тут отттенки одного и того же, без стандартных названий
                    2. Название статьи намекает на что в ней будет разговор про то как писать тесты правильно, а реально — копипаста определений терминов из книжек, об этом и был мой комент
                +2
                >У данной статьи была задача показать, что написание правильных модульных тестов — достаточно сложная задача

                ГДЕ?

                Пока что приведена терминология, но не сама суть.
                  0
                  Комментаторы выше с вами не согласны :))

                  Какая суть-то? Как, прочитав про шаблоны тестовых объектов, теперь их создать в коде?
                  Наверное, использовав оператор new, или аналог.
                  0
                  Еще пробежался по статье — можно также упомянуть Test-Double-объекты (или Test-Specific Subclass) — наследники тестируемых объектов, созданные специально для упрощения тестирования. Я понимаю, что при использовании «Test Spy», они используются для перехвата вызовов, но и сами по себе они заслуживают отдельного внимания.
                    0


                    Ну да, некое «обобщение», но мне кажется это уже некоторый «перебор» что ли… :)

                    Можно еще про 'seams' написать, или про strict и unstrict mocks в приближении уже к более-менее конкретным isolation frameworks.
                      0
                      А, вот какая диаграмма на xunit-е. Мы с командами никогда не называли фейки TestDub-ами. Под TestDub я подразумевал именно Test-Specific Subclass — а у него вполне конкретный сценарий использования, кстати, достаточно часто встречающийся. Так что никакой не перебор.
                    +3
                    Дело ясное, что дело темное

                    Only users with full accounts can post comments. Log in, please.