Страницы

пятница, 28 сентября 2012 г.

Эффективное тестирование. Design for testability.

 

Перед тем, как вернутся к CardFlow хотелось бы сделать большое отступление на тему тестов.   Тесты – важнейший элемент развития кода. Быть может, даже более важный, чем важный рабочий код системы. Подушка безопасности, которая предохраняет программы от ошибок программистов. Как сохранить тесты полезными и рабочими? Я полагаю, здесь есть три важные ортогональные концепции:

  • Поддерживаемость. (Maintainability).
  • Детерминизм. (Determinism).
  • Скорость. (Speed)

  Изначально, я хотел сделать этот список нумерованным, и отсортировать по убыванию важности. Но не смог расставить приоритеты (поэтому расставил в порядке убывания количества букв). Каждая из этих концепций чрезвычайно важна, и пренебрежение любой из них делает тесты обузой. Будь я Мартином Фаулером, ввёл бы специальный акроним – MDS, а тесты написанные с соблюдением этих концепций – MDS Complaint .

  Как должен выглядеть тест, чтобы называться MDS-Complaint:

  1. 100% детерминирован (любите отлаживать обрыв ethernet-кабеля через в дебаггере? я тоже не люблю).
  2. Не существует другого теста на тестируемую функциональность. (DRY, DRY, DRY).
  3. Выполняется менее чем за 20 миллисекунд (1000 тестов менее чем за 20 секунд – мой идеал.).
  4. Зеленеет после рефакторинга кода, если функционал не был повреждён (скажи прощай mock’ам). 
  5. Содержит не более 10 строк кода в самом тесте, и не более 10 в setup-методе. (не люблю долго вникать в суть теста).

  Достичь MDS-совместимости тестов не так уже легко. К счастью, всё (хорошо, почти всё) уже украдено до нас. Вот мой краткий анализ болевых точек, на которые надо обращать внимание при написании тестов. 

Поддерживаемость.

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

  1. Организация тестов – в первую очередь, расположение. Если вы за 10 секунд не смогли найти нужный вам тест в проекте – что-то не так. Местоположение тестов должно быть интуитивно понятно, и подчинено неким соглашениям.
  2. Логика в тестах – очень страшное зло. Логика в тестах зачастую дублирует логику тестируемого кода, поэтому ошибки будут повторятся, и тест будет зелёным. Это страшно на самом деле. 
  3. Mocks – многие мне не поверят, но я абсолютно убеждён: в 95% случаев тесты с mock-объектами не тестируют ничего, кроме правильности установки ожиданий. Малейший рефакторинг приводит к необходимости вносить изменения в ожидания, причём зачастую в нескольких местах. Хотя система по-прежнему остаётся рабочей. Это чрезвычайно досадная печалька.
  4. Количество тестов. Как и любой другой код, тесты требуют поддержки, рефакторинга, создание дополнительных классов и т.д. Мысль проста – меньше тестов – меньше затрат на поддержку (и выше скорость выполнения).
  5. Имена тестов – Как и для методов, классов, интерфейсов и т.д. лучший комментарий для теста – его имя. Если вы не не можете написать хорошее имя для теста – скорее всего он тестирует слишком много всего сразу. Хорошенько подумайте, прежде чем его написать.

Детерминизм.

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

  1. Изоляция - Состояние системы, доставшееся после предыдущих тестов должно быть очищено, и новый тест работает с чистого листа. Всегда. Что это означает на практике? Возьмите перед тестом новый экземпляр объекта, полностью очистите базу данных, удалите файл и т.д. Исключение можно сделать лишь для объектов, состояние которых не влияет на тесты ( скажем, прочтённая из конфигурации строка подключения) ..
  2. Многопоточность. Асинхронные/многопоточные тесты – это всегда пляски с бубном в целях детерминизма. В таких тестах всегда приходится заниматься либо периодическим опросом, либо играться с примитивами синхронизации. И тот и другой способ сильно бьют по удобству поддержки и скорости выполнения тестов. Правильный вариант – тесты должны быть однопоточные, хотя иногда их сложно написать в таком ключе.  
  3. Удалённые сервисы. - Как минимум раз в неделю такой тест будет падать из-за проблем с сетью или самим удалённым сервисом. Исходя из своей практики, тесты с удалённым сервисом никогда не бывают более чем 90% детерминированы. Поэтому никогда не используйте удалённый сервис. Только заглушку, реализующую   интерфейса адаптера к удалённому сервису.
  4. Текущее время. – Настоящее реальное время в тестах ни к чему, поэтому под рукой всегда надо иметь хорошую реализацию фиктивных часов. Один из таких примеров я демонстрировал ранее.

Скорость.

  1. Многопоточность. Из-за склонности многопоточных тестов к периодическому опросу (polling), тесты часто содержат нечто вроде Thread.Sleep(1000). Целая секунда потерянного на сон потока! Неуж-то в это время нельзя заняться чем-то более полезным? Можно! Не пишите таких тестов. Чуть менее вредительский вариант – использование примитивов синхронизации, вроде ManualResetEvent, но в целом проблемы одинаковы – мы тупим в ожидании.
  2. Удалённые сервисы. Как и говорил ранее – межпроцессные вызовы выполнятся на 4-6 порядков медленнее внутрипроцессных. А хорошее тестирование содержит десятки-сотни вызовов удалённых сервисов. Если один вызов длиться 100мс, и тесты содержат 90 вызовов – то нам предстоит полторы минуты тупить в монитор.  Непозволительно долго и бессмысленно. Хорошая заглушка спасёт нас.
  3. Файловая система. Операции с файловой системой очень просты и понятны, писать для них заглушку не хочется. Но жесткие диски (даже SSD) способны изрядно притормозить процесс тестирования, особенно если в фоне на жёсткий диск записывается еще и фильм из интернета Улыбка Что делать если фильм мешает тестам? Правильно – нафиг такие тесты жесткие диски. Виртуальный диск спасёт и скорость, и фильм!
  4. Базы данных. База данных обычно тоже является удалённым сервисом, но отличается  весьма высокой детерминированностью. Чего нельзя сказать о скорости. Поэтому при выборе БД и технологии доступа к ней стоит присмотреться в возможности перевода БД во внутрипроцессный режим (SQLite, SQL Server Compact, RavenDB). А еще лучше и хранить данные в памяти, как это умеет SQLite. Кроме чистой скорости мы выиграем еще и возможность гонять тесты параллельно, без риска испортить тестам контекст.

Справделивости ради, я не единственный, кто проводил такой анализ. Фаулер тоже писал о проблемах тестирования. К сожалению, написал поздно – я набил все шишки самостоятельно. Зато еще раз убедился в правильности собственных выводов.

Design For Testablity.

На пути тестирования поджидает множество опасностей. При проектировании системы все их надо брать в расчёт, и изначально продумывать архитектуру приложения со встроенной поддержкой тестирования. Каждый класс, каждый метод, каждая строчка кода должна быть рассмотрена под микроскопом тестирования.

Комментариев нет:

Отправить комментарий