Устаревший код и запах тестами
Работа с модульными тестами может помочь во многих отношениях улучшить базу кода.
Один из аспектов, который мне больше всего нравится, заключается в том, что тесты могут указать нам на запах кода в рабочем коде.
Например, если тест требует большой настройки или утверждает много выходов, это может указывать на то, что тестируемый модуль не соответствует хорошему дизайну, такому как SRP и другие OOD.
Но иногда сами тесты плохо структурированы или разработаны.
В этом посте я приведу два примера для таких случаев и покажу, как я это решил.
Типы испытаний
(или слои)
Существует несколько типов или слоев тестов.
- Модульные тесты
Модульные тесты должны быть простыми для описания и понимания.
Эти тесты должны выполняться быстро . Они должны проверить одну вещь. Одна единица (метод?) Работы. - Интеграционные тесты
Интеграционные тесты более расплывчаты по определению.
Какие модули они проверяют?
Интеграция нескольких модулей вместе? Проводка инжектора зависимостей?
Тест с использованием реальной БД? - Поведенческие тесты
Эти тесты будут проверять функции.
Они могут быть интерфейсом между PM / PO и командой разработчиков. - End2End / Acceptance / Staging / Functional
Тесты высокого уровня. Может работать в производственной или производственной среде.
Сложность тестов
По сути, чем выше уровень теста, тем он сложнее.
Кроме того, соотношение между возможным количеством тестов и рабочим кодом резко увеличивается на уровне тестирования.
Модульные тесты будут расти линейно по мере роста кода.
Но начиная с интеграционных тестов и тестов более высокого уровня, опции начинают расти в геометрической прогрессии.
Простой расчет:
если два класса взаимодействуют друг с другом, и у каждого есть 2 метода, сколько вариантов мы должны проверить, если мы хотим охватить все варианты? И представьте, что эти методы имеют некоторый поток управления, как если бы .
Спорадически провальные тесты
Есть много причин для того, чтобы тест был «проблематичным».
Одним из худших является тест, который иногда не проходит и обычно проходит.
Команда игнорирует письма CI. Это создает шум в системе.
Вы никогда не можете быть уверены, есть ли ошибка или что-то сломано, или это ложная тревога.
В конце концов мы отключим CI, потому что «он не работает и не стоит времени».
Интеграционный тест и ложная тревога
Любой тип теста является предметом ложных срабатываний, если мы не следуем основным правилам.
Чем выше уровень теста, тем больше вероятность ложных срабатываний.
В интеграционных тестах выше вероятность ложных срабатываний из-за проблем с внешними ресурсами:
нет подключения к Интернету, нет подключения к БД, случайная ошибка и многое другое.
Наша тестовая среда
Наша система «квази-наследие».
Это не совсем наследство, потому что у него есть тесты. Эти тесты даже имеют хорошее покрытие.
Это наследие из-за того, как он (не) структурирован и как построены тесты.
Раньше его охватывали только интеграционные тесты.
В последние несколько месяцев мы начали внедрять юнит-тесты. Особенно на новый код и новые функции.
Все наши интеграционные тесты наследуются от BaseTest , который наследует SpringJ AbstractJUnit4SpringContextTests .
Контекст теста связывает все. Около 95% производственного кода.
Это требует времени, но еще хуже, он подключается к реальным внешним ресурсам, таким как MongoDB и сервисам, которые подключаются к Интернету.
Чтобы повысить скорость тестирования, несколько недель назад я изменил MongoDB на встроенный. Это улучшило время выполнения тестов на порядок.
Этот тип настройки делает тестирование намного сложнее.
Очень сложно издеваться над сервисами. Среда не изолирована от интернета и БД и многое другое.
После этого длинного вступления я хочу описать два проблемных теста и то, как я их исправил.
Их общим недостатком было то, что они иногда терпели неудачу и обычно проходили мимо.
Однако каждый потерпел неудачу по разной причине.
Пример 1 — Создание подключения к Интернету в конструкторе
В первом примере показан тест, который иногда не удался из-за проблем с соединением.
Сложность заключалась в том, что в конструкторе был создан сервис.
Этот сервис получил HttpClient, который также был создан в конструкторе.
Другая проблема заключалась в том, что я не мог изменить тест, чтобы использовать макеты вместо проводки Spring.
Вот оригинальный конструктор (модифицированный для примера):
Тестируемый класс
private HttpClient httpClient; private MyServiceOne myServiceOne; private MyServiceTwo myServiceTwo; public ClassUnderTest(PoolingClientConnectionManager httpConnenctionManager, int connectionTimeout, int soTimeout) { HttpParams httpParams = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(httpParams, connectionTimeout); HttpConnectionParams.setSoTimeout(httpParams, soTimeout); HttpConnectionParams.setTcpNoDelay(httpParams, true); httpClient = new DefaultHttpClient(httpConnenctionManager, httpParams); myServiceOne = new MyServiceOne(httpClient); myServiceTwo = new MyServiceTwo(); }
В тестируемом методе используется myServiceOne .
И тест иногда не удался из-за проблем с подключением в этом сервисе.
Другая проблема заключалась в том, что он не всегда был детерминированным (результат из Интернета) и, следовательно, не удался.
То, как написан код, не позволяет нам издеваться над сервисами.
В тестовом коде тестируемый класс вводился с использованием аннотации @Autowired .
Решение — извлечение и переопределение вызова
Идея была взята из Эффективной работы с Устаревшим Кодексом .
- Определить, что мне нужно исправить.
Чтобы сделать тест детерминированным и без реального подключения к интернету, мне нужен доступ для создания сервисов. - Я представлю защищенные методы, которые создают эти сервисы.
Вместо создания сервисов в конструкторе я буду вызывать эти методы. - В тестовой среде я создам класс, который расширяет тестируемый класс.
Этот класс переопределит эти методы и вернет поддельные (поддельные) сервисы.
Код решения
Тестируемый класс
public ClassUnderTest(PoolingClientConnectionManager httpConnenctionManager, int connectionTimeout, int soTimeout) { HttpParams httpParams = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(httpParams, connectionTimeout); HttpConnectionParams.setSoTimeout(httpParams, soTimeout); HttpConnectionParams.setTcpNoDelay(httpParams, true); this.httpClient = createHttpClient(httpConnenctionManager, httpParams); this.myserviceOne = createMyServiceOne(httpClient); this.myserviceTwo = createMyServiceTwo(); } protected HttpClient createHttpClient(PoolingClientConnectionManager httpConnenctionManager, HttpParams httpParams) { return new DefaultHttpClient(httpConnenctionManager, httpParams); } protected MyServiceOne createMyServiceOne(HttpClient httpClient) { return new MyServiceOne(httpClient); } protected MyServiceTwo createMyServiceTwo() { return new MyServiceTwo(); }
Тестовый класс
private MyServiceOne mockMyServiceOne = mock(MyServiceOne.class); private MyServiceTwo mockMyServiceTwo = mock(MyServiceTwo.class); private HttpClient mockHttpClient = mock(HttpClient.class); private class ClassUnderTestForTesting extends ClassUnderTest { private ClassUnderTestForTesting(int connectionTimeout, int soTimeout) { super(null, connectionTimeout, soTimeout); } @Override protected HttpClient createHttpClient(PoolingClientConnectionManager httpConnenctionManager, HttpParams httpParams) { return mockHttpClient; } @Override protected MyServiceOne createMyServiceOne(HttpClient httpClient) { return mockMyServiceOne; } @Override protected MyServiceTwo createMyServiceTwo() { return mockMyServiceTwo; } }
Теперь вместо связывания тестируемого класса я создал его в методе @Before .
Он принимает другие услуги (не описанные здесь). Я получил эти сервисы, используя @Autowire.
Еще одно замечание: перед созданием специального класса для теста я запустил все интеграционные тесты этого класса, чтобы убедиться, что рефакторинг ничего не сломал.
Я также перезапустил сервер локально и проверил, что все работает.
Это важно сделать при работе с устаревшим кодом.
Пример 2 — Статистические тесты для случайного ввода
Второй пример описывает тест, который не удался из-за случайных результатов и статистического утверждения.
Код сделал случайный выбор между объектами с похожими атрибутами (здесь я упрощаю сценарий).
Случайный объект был создан в конструкторе класса.
Упрощенный пример:
private Random random; public ClassUnderTest() { random = new Random(); // more stuff } //The method is package protected so we can test it MyPojo select(List<MyPojo> pojos) { // do something int randomSelection = random.nextInt(pojos.size()); // do something return pojos.get(randomSelection); }
Исходный тест сделал статистический анализ.
Я просто объясню это, поскольку это слишком сложно и многословно, чтобы написать это.
У него был цикл из 10 тысяч итераций. Каждая итерация вызывала тестируемый метод.
У него была Карта, которая подсчитывала количество вхождений (возвращаемый результат) на MyPojo.
Затем он проверил, был ли выбран каждый MyPojo в (10K / Number-Of-MyPojo) с некоторым отклонением 0,1.
Пример:
скажем, у нас есть 4 экземпляра MyPojo в списке.
Затем утверждение подтвердило, что каждый экземпляр был выбран между 2400 и 2600 раз (10K / 4) с отклонением 10%.
Конечно, можно ожидать, что иногда тест не удался. Увеличение отклонения только уменьшит количество ложных неудачных испытаний.
Решение — перегрузить метод
- Перегрузка тестируемого метода.
В перегруженном методе добавьте параметр, который совпадает с глобальным полем. - Переместите код из оригинального метода в новый.
Убедитесь, что вы используете параметр метода, а не поле класса. Здесь могут помочь разные имена. - Проверяет вновь созданный метод с помощью mock.
Код решения
private Random random; // Nothing changed in the constructor public ClassUnderTest() { random = new Random(); // more stuff } // Overloaded method private select(List<MyPojo> pojos) { return select(pojos, this.random); } //The method is package protected so we can test it MyPojo select(List<MyPojo> pojos, boolean inRandom) { // do something int randomSelection = inRandom.nextInt(pojos.size()); // do something return pojos.get(randomSelection); }
Вывод
Работа с устаревшим кодом может быть сложной и увлекательной.
Работа с устаревшим тестовым кодом также может быть увлекательной.
Очень приятно перестать получать раздражающие письма о неудачных тестах.
Это также повышает доверие команды к процессу КИ.