Почему вам нужно перестать писать модульные тесты

Почему вам нужно перестать писать модульные тесты

22 февраля 2022 г.

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


Давайте начнем с того, почему:


Почему мы тестируем?


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


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

  • Тесты как документация и спецификация — Тесты должны объяснять и определять поведение вашего кода. У вас есть цель во время тестирования, и обычно эта цель составляет весь смысл самого кода, поэтому чем яснее и конкретнее будут ваши тесты, тем лучше.

  • Тесты как локаторы ошибок — Если вы Шерлок Холмс, ваши тесты — Джон Ватсон, если какая-то функция была убита, вы хотите найти виновного, и быстро. Ваши тесты (надеюсь) точно определят место убийства, так что вы сможете начать анализировать и устранять неразбериху. Чем лучше и атомарнее они определяют, тем быстрее вы сможете добраться до сути дела.


Максимальная уверенность


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


Мы хотим убедиться, что когда наше приложение дает сбой, наши тесты не работают, а когда наше приложение работает, наши тесты работают. Это наш главный принцип.


Но как мы можем максимизировать эту уверенность?


Проверка поведения


Мы можем добиться этого, проверив, как именно мы хотим, чтобы он вел себя. Мы видим новые практики, такие как TDD и BDD, предназначенные для максимизации ПО, ориентированное на поведение.


«Чем больше ваши тесты походят на то, как используется ваше программное обеспечение, тем больше уверенности они могут дать вам». - Кент С. Доддс


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



Правило рефакторинга


«Рефакторинг — это метод реструктуризации существующего кода, изменение его внутренней структуры без изменения его внешнего поведения». — Мартин Фаулер


Поэтому, если нас интересует только поведение при тестировании, логично предположить, что рефакторинг не должен приводить к провалу тестов.


Это правило рефакторинга. Когда вы реорганизуете свой код, ваши тесты все еще должны проходить. Все они.


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


Но как мы можем этого добиться? Как мы можем тестировать поведение, не затуманивая наши тесты спецификой?


Методические рекомендации


Если это приватно, не проверяйте его


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


Но не бери у меня, бери у Кента:



Ссылка


Попробуйте TDD


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


Помните, что ваши тесты не должны быть вторичной реализацией.


Правильно называйте свои тесты


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


Всегда программируют как минимум два человека: ты и ты через два месяца.


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


📝 Я предпочитаю шаблон ДОЛЖЕН… КОГДА классическому ДАННО -> КОГДА -> ТОГДА и причина проста. Инженеры спешат, и объяснение «тогда» обычно экономит немного времени. Почти как TL;DR.



Имитация только источников данных


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


Обычно единственными фиксированными частями вашего приложения являются ваши источники данных. Если вы тестируете обработчик, вы знаете, что он получает данные из шлюза, так что это единственное, что вы имитируете (возможно, вы даже можете сказать, что ваши данные поступают из базы данных, таким образом только имитируя базу данных Но пока вы следуете [принципу инверсии зависимостей] (https://en.wikipedia.org/wiki/Dependency_inversion_principle), вы должны быть в порядке, издеваясь над этим шлюзом).


Если вы тестируете шлюз, вы знаете, что он получает данные с внешнего сервера, поэтому вы имитируете этот сервер.


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


Рассмотрим следующий пример:



Здесь у нас есть модуль (Дом) и два подмодуля (Окно и Дверь).


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


У нас может возникнуть соблазн вызвать безопасный метод, а затем убедиться, что наше окно и дверь закрыты. Для этого мы будем имитировать Window и Door и утверждать их вызовы методов close.


Это просто неправильно, так как мы в основном дублируем нашу реализацию.


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


Что, если я добавлю гараж и забуду вызвать его метод close() для метода secure() своего дома? Теперь мои тесты проходят, и мое приложение не работает. Красные флаги повсюду.


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


🟨⚠️ Хорошо, но теперь наши домашние тесты зависят от реализации окон и дверей! Это похоже на интеграционный тест!


Точно моя точка зрения.


Новый подход к тестированию


Разработка программного обеспечения развивалась, как и связанные с ней методологии. Мы стали свидетелями растущей популярности Agile с быстрыми циклами итераций и CI/CD. Но при этом ставки ремонтопригодности выше, чем когда-либо. Ремонтопригодность — это не просто то, что хорошо иметь, это главный приоритет.


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


«Напишите тесты. Не так много. В основном интеграция». — Гильермо Раух


Вы хотите максимизировать тестирование поведения, и это поведение каскадируется до зависимостей модулей (это имеет смысл, поскольку очевидно, что они нужны для правильной работы).


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


Не все так радужно


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


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



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


Заключение


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


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


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


И спасибо, если вы зашли так далеко!



Удачного тестирования!


  • Впервые опубликовано [здесь] (https://alramalho.medium.com/improve-your-codes-maintainability-by-dropping-unit-tests-92f115d5aa6b)*


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE