Перед тем, как вернутся к CardFlow хотелось бы сделать большое отступление на тему тестов. Тесты – важнейший элемент развития кода. Быть может, даже более важный, чем важный рабочий код системы. Подушка безопасности, которая предохраняет программы от ошибок программистов. Как сохранить тесты полезными и рабочими? Я полагаю, здесь есть три важные ортогональные концепции:
- Поддерживаемость. (Maintainability).
- Детерминизм. (Determinism).
- Скорость. (Speed)
Изначально, я хотел сделать этот список нумерованным, и отсортировать по убыванию важности. Но не смог расставить приоритеты (поэтому расставил в порядке убывания количества букв). Каждая из этих концепций чрезвычайно важна, и пренебрежение любой из них делает тесты обузой. Будь я Мартином Фаулером, ввёл бы специальный акроним – MDS, а тесты написанные с соблюдением этих концепций – MDS Complaint .
Как должен выглядеть тест, чтобы называться MDS-Complaint:
- 100% детерминирован (любите отлаживать обрыв ethernet-кабеля через в дебаггере? я тоже не люблю).
- Не существует другого теста на тестируемую функциональность. (DRY, DRY, DRY).
- Выполняется менее чем за 20 миллисекунд (1000 тестов менее чем за 20 секунд – мой идеал.).
- Зеленеет после рефакторинга кода, если функционал не был повреждён (скажи прощай mock’ам).
- Содержит не более 10 строк кода в самом тесте, и не более 10 в setup-методе. (не люблю долго вникать в суть теста).
Достичь MDS-совместимости тестов не так уже легко. К счастью, всё (хорошо, почти всё) уже украдено до нас. Вот мой краткий анализ болевых точек, на которые надо обращать внимание при написании тестов.
Поддерживаемость.
Как и код другой системы, тесты не пишутся раз навсегда. Их приходится пересматривать (а может и переписывать) на ежедневной основе. Одни устарели, другие требует дополнительной инициализации после изменений в коде системы, третьи вдруг начали ловить ошибку, которой раньше никто не замечал. И есть несколько факторов, которые могут испортить поддерживаемость тестов:
- Организация тестов – в первую очередь, расположение. Если вы за 10 секунд не смогли найти нужный вам тест в проекте – что-то не так. Местоположение тестов должно быть интуитивно понятно, и подчинено неким соглашениям.
- Логика в тестах – очень страшное зло. Логика в тестах зачастую дублирует логику тестируемого кода, поэтому ошибки будут повторятся, и тест будет зелёным. Это страшно на самом деле.
- Mocks – многие мне не поверят, но я абсолютно убеждён: в 95% случаев тесты с mock-объектами не тестируют ничего, кроме правильности установки ожиданий. Малейший рефакторинг приводит к необходимости вносить изменения в ожидания, причём зачастую в нескольких местах. Хотя система по-прежнему остаётся рабочей. Это чрезвычайно досадная печалька.
- Количество тестов. Как и любой другой код, тесты требуют поддержки, рефакторинга, создание дополнительных классов и т.д. Мысль проста – меньше тестов – меньше затрат на поддержку (и выше скорость выполнения).
- Имена тестов – Как и для методов, классов, интерфейсов и т.д. лучший комментарий для теста – его имя. Если вы не не можете написать хорошее имя для теста – скорее всего он тестирует слишком много всего сразу. Хорошенько подумайте, прежде чем его написать.
Детерминизм.
Недетерминированные тесты бесполезны. Пройдёт немного времени, и все перестанут обращать внимание на количество красных тестов, и их связь со свеженаписанным кодом. Это приводит к постепенному регрессу в качестве. Если есть недетерминированный тест – его лучше просто отключить, нежели наслаждаться перецветами. По крайней мере, сохраните качество кода, покрытого оставшимися тестами.
- Изоляция - Состояние системы, доставшееся после предыдущих тестов должно быть очищено, и новый тест работает с чистого листа. Всегда. Что это означает на практике? Возьмите перед тестом новый экземпляр объекта, полностью очистите базу данных, удалите файл и т.д. Исключение можно сделать лишь для объектов, состояние которых не влияет на тесты ( скажем, прочтённая из конфигурации строка подключения) ..
- Многопоточность. Асинхронные/многопоточные тесты – это всегда пляски с бубном в целях детерминизма. В таких тестах всегда приходится заниматься либо периодическим опросом, либо играться с примитивами синхронизации. И тот и другой способ сильно бьют по удобству поддержки и скорости выполнения тестов. Правильный вариант – тесты должны быть однопоточные, хотя иногда их сложно написать в таком ключе.
- Удалённые сервисы. - Как минимум раз в неделю такой тест будет падать из-за проблем с сетью или самим удалённым сервисом. Исходя из своей практики, тесты с удалённым сервисом никогда не бывают более чем 90% детерминированы. Поэтому никогда не используйте удалённый сервис. Только заглушку, реализующую интерфейса адаптера к удалённому сервису.
- Текущее время. – Настоящее реальное время в тестах ни к чему, поэтому под рукой всегда надо иметь хорошую реализацию фиктивных часов. Один из таких примеров я демонстрировал ранее.
Скорость.
- Многопоточность. Из-за склонности многопоточных тестов к периодическому опросу (polling), тесты часто содержат нечто вроде Thread.Sleep(1000). Целая секунда потерянного на сон потока! Неуж-то в это время нельзя заняться чем-то более полезным? Можно! Не пишите таких тестов. Чуть менее вредительский вариант – использование примитивов синхронизации, вроде ManualResetEvent, но в целом проблемы одинаковы – мы тупим в ожидании.
- Удалённые сервисы. Как и говорил ранее – межпроцессные вызовы выполнятся на 4-6 порядков медленнее внутрипроцессных. А хорошее тестирование содержит десятки-сотни вызовов удалённых сервисов. Если один вызов длиться 100мс, и тесты содержат 90 вызовов – то нам предстоит полторы минуты тупить в монитор. Непозволительно долго и бессмысленно. Хорошая заглушка спасёт нас.
- Файловая система. Операции с файловой системой очень просты и понятны, писать для них заглушку не хочется. Но жесткие диски (даже SSD) способны изрядно притормозить процесс тестирования, особенно если в фоне на жёсткий диск записывается еще и фильм из интернета
Что делать если фильм мешает тестам? Правильно – нафиг
такие тестыжесткие диски. Виртуальный диск спасёт и скорость, и фильм! - Базы данных. База данных обычно тоже является удалённым сервисом, но отличается весьма высокой детерминированностью. Чего нельзя сказать о скорости. Поэтому при выборе БД и технологии доступа к ней стоит присмотреться в возможности перевода БД во внутрипроцессный режим (SQLite, SQL Server Compact, RavenDB). А еще лучше и хранить данные в памяти, как это умеет SQLite. Кроме чистой скорости мы выиграем еще и возможность гонять тесты параллельно, без риска испортить тестам контекст.
Справделивости ради, я не единственный, кто проводил такой анализ. Фаулер тоже писал о проблемах тестирования. К сожалению, написал поздно – я набил все шишки самостоятельно. Зато еще раз убедился в правильности собственных выводов.
Design For Testablity.
На пути тестирования поджидает множество опасностей. При проектировании системы все их надо брать в расчёт, и изначально продумывать архитектуру приложения со встроенной поддержкой тестирования. Каждый класс, каждый метод, каждая строчка кода должна быть рассмотрена под микроскопом тестирования.