Отсутствие внутреннего состояния и то, как это упрощает тестирование и рефакторинг ваших классов

Отсутствие внутреннего состояния и то, как это упрощает тестирование и рефакторинг ваших классов

6 мая 2022 г.

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


Отсутствие внутреннего состояния и тестируемости


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


  1. Приведите экземпляр в состояние, которое хотите протестировать.

  1. Проверьте его поведение

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


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


Перемещение состояния


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


Пример с низкой тестируемостью


В качестве примера с низкой тестируемостью у нас будет класс будильника:


Описание изображения


Мы можем ожидать определенного поведения от этих часов:


  • часы звонят, когда текущее время совпадает с запрограммированным временем будильника

  • пользователь может установить время будильника

Если вы хотите проверить это поведение, вам понадобится один из двух подходов:


  1. подождите, пока не наступит жестко заданное время будильника, и посмотрите, звенит ли будильник

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

Подход 1 неверен; это может потребовать часов ожидания.


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


Более проверяемый пример


Мы можем сделать этот код более тестируемым, переместив состояние наружу:


Описание изображения


Итак, в этом случае введите зависимость — AlarmTimeStore — которая хранит значение, установленное вне класса AlarmClock.


Как это упрощает тестирование


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


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


Описание изображения


Как это делает код лучше


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


  • сделать его более продвинутым: например, вместо сохранения значения в оперативной памяти, сохранить его в браузере или в файле, или

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

Общая критика: так много уровней абстракции


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


Как написание тестов влияет на ваш код?


Вы регулярно пишете тесты? Пожалуйста, расскажите, как это повлияет на ваш код — ребята, мне бы хотелось услышать от вас истории!


Также опубликовано [Здесь] (https://how-to.dev/how-the-lack-of-internal-state-makes-your-classes-easier-to-test-and-refactor)



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