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

Однако бывают и сложные случаи.

  1. Первый случай — это тесты, воспроизводящие поведение библиотек. Типичный случай — http-client из какой-нибудь библиотеки, где нужно создать клиента, запихнуть ему куки и заголовки и послать запрос. Смысла тестировать каждый шаг нет, там еще могут попасться статические методы, new и прочие тестабилити-киллеры, поэтому проще всего завернуть его в тонкий класс-обертку. Юнит-тесты такому классу не нужны. Тестировать этот класс можно в контексте интеграционных тестов зависимостей, то есть мы вызываем его против реального сервиса и убеждаемся, что библиотека работает как ожидается. Важно, чтобы класс-обертка не содержал дополнительной логики, которая может быть протестирована в рамках юнит-теста.
  2. Второй случай — это тесты, проверяющие что-то, что не возвращает результата (или возвращает невнятное), соответственно, единственный способ проверить, что что-то было вызвано — это Spy со счетчиком. Это валидный кейс, хотя и ограниченный. Сюда могут относиться вызванные хранимые процедуры, отсылки писем.
  3. Третий случай — это тесты стратегий, политик и прочих behavioral patterns. Например, мы хотим, чтобы при создании заказа у нас попала запись в базу, потом он встал в очередь заказов, а затем что-то записалось в логи и емейлы.

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

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

Комментарии (12)


  1. iLikeKoffee
    14.11.2019 03:29

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


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


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


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


    Итого на какой-нибудь сценарий с пустым юзернеймом или емейлом без собачки может быть 3 разных теста (а еще в QA в e2e тестах может написать что-то подобное), которые вроде повторяются, но тестируют разное.


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


    1. rudnevr Автор
      14.11.2019 04:07

      то, что вы описываете, это не тавтологические тесты. Тавтологические тесты — это тесты, которые повторяют имплементацию.

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


      1. ggo
        14.11.2019 09:34
        +1

        Наличие хороших юнит-тестов не отменяет пользу и существование интеграционных тестов.
        Интеграционные тесты могут покрывать в том числе логику настройки окружения, логику сценариев деплоя, и т.д.


        1. rudnevr Автор
          14.11.2019 09:45

          Приведите пример какой нибудь, трудно абстрактно обсуждать


          1. ggo
            14.11.2019 09:54

            А правильный ли урл указан в качестве точки подключения к БД?
            А стартует ли вообще наш докер образ и сервис слушается на нужном порте?
            И т.д.
            Очевидно, это полезно проверять. Модульными тестами такое не проверить.


            1. rudnevr Автор
              14.11.2019 11:29
              +1

              Насчёт урла я не понимаю. Урл — это внешний параметр, тестировать его бесполезно. Работающий дев урл ничего не говорит о QA или продакшене.


              Про докер я не знаю, что вы конкретно имеете ввиду — если у нас приложение внутри докера, то он опять же внешний получается компонент.


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


              1. ggo
                16.11.2019 08:38

                Если команда отвечает за сервис от начала до конца, в том числе за конфигурации, которые как все артефакты лежат в git-репозитарии и проходят обычный цикл разработки (планирование-реализация-тестирование-доставка), то да, появляются тесты на докер, урлы бд, и т.д.


                1. rudnevr Автор
                  16.11.2019 10:31

                  если команда отвечает за сервис от начала до конца, это не значит, что нужно пренебрегать практиками. Cloud-native, 12-factor и тп
                  Интеграционные тесты приложения вместе с окружающей средой вредят и приложению, и окружающей среде. Индустрия в принципе движется туда, где эти вещи ортогональны.


    1. ggo
      14.11.2019 09:51
      +2

      Я в интеграционных тестах концентрируюсь на:
      — happy path
      — очень ограниченное количество fail case
      Дальше из эксплуатации станет понятно, какие кейсы в интеграционные тесты добавить.

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

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


      1. rudnevr Автор
        14.11.2019 11:58

        Я в интеграционных тестах концентрируюсь на:
        — happy path
        — очень ограниченное количество fail case
        Дальше из эксплуатации станет понятно, какие кейсы в интеграционные тесты добавить.


        Это знакомо, так девяносто процентов делает, т. е. фактически юзеры тестируют.


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


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


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


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


        1. ggo
          16.11.2019 08:57

          Это знакомо, так девяносто процентов делает, т. е. фактически юзеры тестируют

          Интересный вывод конечно ;)

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

          мы решили что будем экономить на тестах и поэтому тестировать только места со сложной логикой.

          Тоже интересный вывод…

          Мой посыл был только в том, что тестируем то, что есть в текущей реализации.
          Есть в веб-компоненте сериализация/десериализация — тестируем сериализацию/десериализацию. Отдана валидация в стороннюю компоненту — значит здесь тестируем что передача в валидацию происходит правильно. А непосредственно валидацию тестируем в проекте компоненты валидации. Это общий случай, условная норма. Бывает много частных с отклонением от нормы.


          1. rudnevr Автор
            16.11.2019 10:28

            >> Мой посыл был только в том, что тестируем то, что есть в текущей реализации.

            Этому я совершенно не противоречу. Свой код покрывается юнитами, внешние контракты контрактными тестами, то бишь тестами зависимостей.

            Мне показалось, что у вас есть избирательность в том, что тестировать, и прошу прощения, если это не так.