Юнит-тестирование — это процесс проверки отдельных модулей программы на корректность работы. Правильный подход к тестированию позволит максимизировать качество и скорость разработки проекта. Некачественные тесты, наоборот, могут нанести вред: нарушить работоспособность кода, увеличить количество ошибок, растянуть сроки и затраты. Грамотное внедрение юнит-тестирования — хорошее решение для развития проекта.
Научитесь разрабатывать тесты профессионального уровня, без ошибок автоматизировать процессы тестирования, а также интегрировать тестирование в жизненный цикл приложения. Со временем вы овладеете особым чутьем, присущим специалистам по тестированию. Как ни удивительно, практика написания хороших тестов способствует созданию более качественного кода. В этой книге: универсальные рекомендации по оценке тестов; тестирование для выявления и исключения антипаттернов; рефакторинг тестов вместе с рабочим кодом; использование интеграционных тестов для проверки всей системы.
Author(s): Владимир Хориков
Series: Для профессионалов
Publisher: Питер
Year: 2021
Language: Russian
Pages: 320
City: СПб.
Tags: testing
Оглавление
Предисловие к русскому изданию
Предисловие к оригинальному изданию
Благодарности
О книге
Для кого написана эта книга
Структура книги
О коде
Форум для обсуждения книги
Другие сетевые ресурсы
Об авторе
Иллюстрация на обложке
От издательства
Часть I. Общая картина
Глава 1. Цель юнит-тестирования
1.1. Текущее состояние дел в юнит-тестировании
1.2. Цель юнит-тестирования
1.2.1. В чем разница между плохими и хорошими тестами?
1.3. Использование метрик покрытия для оценки качества тестов
1.3.1. Метрика покрытия code coverage
1.3.2. Branch coverage
1.3.3. Проблемы с метриками покрытия
1.3.4. Процент покрытия как цель
1.4. Какими должны быть успешные тесты?
1.4.1. Интеграция в цикл разработки
1.4.2. Проверка только самых важных частей кода
1.4.3. Максимальная защита от багов при минимальных затратах на сопровождение
1.5. Что вы узнаете из книги
Итоги
Глава 2. Что такое юнит-тест?
2.1. Определение юнит-теста
2.1.1. Вопрос изоляции: лондонская школа
2.1.2. Проблема изоляции: классический подход
2.2. Классическая и лондонская школы юнит‑тестирования
2.2.1. Работа с зависимостями в классической и лондонской школе
2.3. Сравнение классической и лондонской школ юнит‑тестирования
2.3.1 Юнит-тестирование одного класса за раз
2.3.2. Юнит-тестирование большого графа взаимосвязанных классов
2.3.3. Выявление точного местонахождения ошибки
2.3.4. Другие различия между классической и лондонской школой
2.4. Интеграционные тесты в двух школах
2.4.1. Сквозные (end-to-end) тесты как подмножество интеграционных тестов
Итоги
Глава 3. Анатомия юнит-теста
3.1. Структура юнит-теста
3.1.1. Паттерн AAA
3.1.2. Избегайте множественных секций arrange, act и assert
3.1.3. Избегайте команд if в тестах
3.1.4. Насколько большой должна быть каждая секция?
Секция подготовки — самая большая
Избегайте секций действий, состоящих из нескольких строк
3.1.5. Сколько проверок должна содержать секция проверки?
3.1.6. Нужна ли завершающая (teardown) фаза?
3.1.7. Выделение тестируемой системы
3.1.8. Удаление комментариев «arrange/act/assert» из тестов
3.2. Фреймворк тестирования xUnit
3.3. Повторное использование тестовых данных между тестами
3.3.1. Сильная связность (high coupling) между тестами как антипаттерн
3.3.2. Использование конструкторов в тестах ухудшает читаемость
3.3.3. Более эффективный способ переиспользования тестовых данных
3.4. Именование юнит-тестов
3.4.1. Рекомендации по именованию юнит-тестов
3.4.2. Пример: переименование теста в соответствии с рекомендациями
3.5. Параметризованные тесты
3.5.1. Генерирование данных для параметризованных тестов
3.6. Использование библиотек для дальнейшего улучшения читаемости тестов
Итоги
Часть II. Обеспечение эффективной работы ваших тестов
Глава 4. Четыре аспекта хороших юнит-тестов
4.1. Четыре аспекта хороших юнит-тестов
4.1.1. Первый аспект: защита от багов
4.1.2. Второй столп: устойчивость к рефакторингу
4.1.3. Что приводит к ложным срабатываниям?
4.1.4. Тестирование конечного результата вместо деталей имплементации
4.2. Связь между первыми двумя атрибутами
4.2.1. Максимизация точности тестов
4.2.2. Важность ложных и ложноотрицательных срабатываний: динамика
4.3. Третий и четвертый аспекты: быстрая обратная связь и простота поддержки
4.4. В поисках идеального теста
4.4.1. Возможно ли создать идеальный тест?
4.4.2. Крайний случай №1: сквозные (end-to-end) тесты
4.4.3. Крайний случай № 2: тривиальные тесты
4.4.4. Крайний случай №3: хрупкие тесты
4.4.5. В поисках идеального теста: результаты
4.5. Известные концепции автоматизации тестирования
4.5.1. Пирамида тестирования
4.5.2. Выбор между тестированием по принципу «черного ящика» и «белого ящика»
Итоги
Глава 5. Моки и хрупкость тестов
5.1 Отличия моков от стабов
5.1.1 Разновидности тестовых заглушек
5.1.2. Мок-инструмент и мок-тестовая-заглушка
5.1.3. Не проверяйте взаимодействия со стабами
5.1.4. Использование моков вместе со стабами
5.1.5. Связь моков и стабов с командами и запросами
5.2. Наблюдаемое поведение и детали имплементации
5.2.1. Наблюдаемое поведение — не то же самое, что публичный API
5.2.2. Утечка деталей имплементации: пример с операцией
5.2.3. Хорошо спроектированный API и инкапсуляция
5.2.4. Утечка деталей имплементации: пример с состоянием
5.3. Связь между моками и хрупкостью тестов
5.3.1. Определение гексагональной архитектуры
5.3.2 Внутрисистемные и межсистемные взаимодействия
5.3.3. Внутрисистемные и межсистемные взаимодействия: пример
5.4. Еще раз о различиях между классической и лондонской школой юнит-тестирования
5.4.1. Не все внепроцессные зависимости должны заменяться моками
5.4.2. Использование моков для проверки поведения
Итоги
Глава 6. Стили юнит-тестирования
6.1. Три стиля юнит-тестирования
6.1.1. Проверка выходных данных
6.1.2. Проверка состояния
6.1.3. Проверка взаимодействий
6.2. Сравнение трех стилей юнит-тестирования
6.2.1. Сравнение стилей по метрикам защиты от багов и быстроте обратной связи
6.2.2. Сравнение стилей по метрике устойчивости к рефакторингу
6.2.3. Сравнение стилей по метрике простоты поддержки
Сопровождаемость тестов, проверяющих выходные данные
Сопровождаемость тестов, проверяющих состояние
Сопровождаемость тестов, проверяющих взаимодействия
6.2.4. Сравнение стилей: результаты
6.3. Функциональная архитектура
6.3.1. Что такое функциональное программирование?
6.3.2 Что такое функциональная архитектура?
6.3.3. Сравнение функциональных и гексагональных архитектур
6.4. Переход на функциональную архитектуру и тестирование выходных данных
6.4.1. Система аудита
6.4.2. Использование моков для отделения тестов от файловой системы
6.4.3. Рефакторинг для перехода на функциональную архитектуру
6.4.4. Потенциальные будущие изменения
6.5. Недостатки функциональной архитектуры
6.5.1. Применимость функциональной архитектуры
6.5.2. Недостатки по быстродействию
6.5.3. Увеличение размера кодовой базы
Итоги
Глава 7. Рефакторинг для получения эффективных юнит-тестов
7.1. Определение кода для рефакторинга
7.1.1. Четыре типа кода
7.1.2. Использование паттерна «Простой объект» для разделения переусложненного кода
7.2. Рефакторинг для получения эффективных юнит‑тестов
7.2.1. Знакомство с системой управления клиентами
7.2.2. Версия 1: преобразование неявных зависимостей в явные
7.2.3 Версия 2: уровень сервисов приложения
7.2.4. Версия 3: вынесение сложности из сервисов приложения
7.2.5. Версия 4: Новый класс Company
7.3. Анализ оптимального покрытия юнит-тестов
7.3.1. Тестирование слоя предметной области и вспомогательного кода
7.3.2. Тестирование кода из трех других четвертей
7.3.3. Нужно ли тестировать предусловия?
7.4. Условная логика в контроллерах
7.4.1. Паттерн «CanExecute/Execute»
7.4.2. Использование доменных событий для отслеживания изменений доменной модели
7.5. Заключение
Итоги
Часть III. Интеграционное тестирование
Глава 8. Для чего нужно интеграционное тестирование?
8.1. Что такое интеграционный тест?
8.1.1. Роль интеграционных тестов
8.1.2. Снова о пирамиде тестирования
8.1.3. Интеграционное тестирование и принцип Fail Fast
8.2. Какие из внепроцессных зависимостей должны проверяться напрямую
8.2.1. Два типа внепроцессных зависимостей
8.2.2. Работа с управляемыми и неуправляемыми зависимостями
8.2.3. Что делать, если вы не можете использовать реальную базу данных в интеграционных тестах?
8.3. Интеграционное тестирование: пример
8.3.1. Какие сценарии тестировать?
8.3.2. Классификация базы данных и шины сообщений
8.3.3. Как насчет сквозного тестирования?
8.3.4. Интеграционное тестирование: первая версия
8.4. Использование интерфейсов для абстрагирования зависимостей
8.4.1. Интерфейсы и слабая связность
8.4.2. Зачем использовать интерфейсы для внепроцессных зависимостей?
8.4.3. Использование интерфейсов для внутрипроцессных зависимостей
8.5. Основные приемы интеграционного тестирования
8.5.1. Явное определение границ модели предметной области
8.5.2. Сокращение количества слоев
8.5.3. Исключение циклических зависимостей
8.5.4. Использование нескольких секций действий в тестах
8.6. Тестирование функциональности логирования
8.6.1. Нужно ли тестировать функциональность логирования?
8.6.2. Как тестировать функциональность логирования?
Создание обертки поверх ILogger
Структурированное логирование
Написание тестов для служебного и диагностического логирования
8.6.3. Какой объем логирования можно считать достаточным?
8.6.4. Как передавать экземпляры логеров?
8.7. Заключение
Итоги
Глава 9. Рекомендации при работе с моками
9.1. Достижение максимальной эффективности моков
9.1.1. Проверка взаимодействий на границах системы
9.1.2. Замена моков шпионами
9.1.3. Как насчет IDomainLogger?
9.2. Практики мокирования
9.2.1. Моки только для интеграционных тестов
9.2.2. Несколько моков на тест
9.2.3. Проверка количества вызовов
9.2.4. Используйте моки только для принадлежащих вам типов
Итоги
Глава 10. Тестирование базы данных
10.1. Предусловия для тестирования базы данных
10.1.1. Хранение базы данных в системе контроля версий
10.1.2. Справочные данные являются частью схемы базы данных
10.1.3. Отдельный экземпляр для каждого разработчика
10.1.4. Развертывание базы данных на основе состояния и на основе миграций
Метод на основе состояния
Метод на основе миграций
Метод на основе миграций лучше метода на основе состояния
10.2. Управление транзакциями
10.2.1 Управление транзакциями в рабочем коде
Отделение подключений к базе данных от транзакций
Обновление транзакции до unit of work
10.2.2. Управление транзакциями в интеграционных тестах
10.3. Жизненный цикл тестовых данных
10.3.1. Параллельное или последовательное выполнение тестов?
10.3.2. Очистка данных между запусками тестов
10.3.3. Не используйте базы данных в памяти
10.4. Переиспользование кода в секциях тестов
10.4.1. Переиспользование кода в секциях подготовки
Где размещать фабричные методы
10.4.2. Переиспользование кода в секциях действий
10.4.3. Повторное использование кода в секциях проверки
10.4.4. Не создает ли тест слишком много транзакций?
10.5. Типичные вопросы при тестировании баз данных
10.5.1. Нужно ли тестировать операции чтения?
10.5.2. Нужно ли тестировать репозитории?
Высокие затраты на сопровождение
Ухудшенная защита от багов
10.6. Завершение
Итоги
Часть IV. Антипаттерны юнит-тестирования
Глава 11. Антипаттерны юнит‑тестирования
11.1. Юнит-тестирование приватных методов
11.1.1. Приватные методы и хрупкость тестов
11.1.2. Приватные методы и недостаточное покрытие
11.1.3. Когда тестирование приватных методов допустимо
11.2. Раскрытие приватного состояния
11.3. Утечка доменных знаний в тесты
11.4. Загрязнение кода
11.5. Мокирование конкретных классов
11.6. Работа со временем
11.6.1. Время как неявный контекст
11.6.2. Время как явная зависимость
11.7. Завершение
Итоги