Устаревший код и запах тестами
Работа с модульными тестами может помочь во многих отношениях улучшить базу кода.
Один из аспектов, который мне больше всего нравится, заключается в том, что тесты могут указать нам на запах кода в рабочем коде.
Например, если тесту требуется большая настройка или заявлено много выходов, это может указывать на то, что тестируемый модуль не соответствует хорошему дизайну, такому как SRP и другие OOD.
Но иногда сами тесты плохо структурированы или разработаны.
В этом посте я приведу два примера для таких случаев и покажу, как я это решил.
Типы испытаний
(или слои)
Существует несколько типов или слоев тестов.
- Модульные тесты
Модульный тест должен быть простым для описания и понимания. Эти тесты должны выполняться быстро . Они должны проверить одну вещь. Одна единица (метод?) Работы. - Интеграционные тесты
Интеграционные тесты более расплывчаты по определению. Какие модули они проверяют? Интеграция нескольких модулей вместе? Проводка инжектора зависимостей? Тест с использованием реальной БД? - Поведенческие тесты
Эти тесты будут проверять функции. Они могут быть интерфейсом между PM / PO и командой разработчиков. - End2End / Acceptance / Staging / Functional
Тесты высокого уровня. Может работать в производственной или производственной среде.
Сложность тестов
По сути, чем выше уровень теста, тем он сложнее. Кроме того, соотношение между возможным количеством тестов и рабочим кодом резко увеличивается на уровне тестирования. Модульные тесты будут расти линейно по мере роста кода. Но начиная с интеграционных тестов и тестов более высокого уровня, опции начинают расти в геометрической прогрессии.
Простой расчет: если два класса взаимодействуют друг с другом, и у каждого есть 2 метода, сколько вариантов мы должны проверить, если мы хотим охватить все варианты? И представьте, что эти методы имеют некоторый поток управления, как если бы .
Спорадически провальные тесты
Есть много причин для того, чтобы тест был «проблематичным». Одним из худших является тест, который иногда не проходит и обычно проходит. Команда игнорирует письма CI. Это создает шум в системе. Вы никогда не можете быть уверены, есть ли ошибка или что-то сломано, или это ложная тревога. В конце концов мы отключим CI, потому что «он не работает и не стоит времени».
Интеграционный тест и ложная тревога
Любой тип теста является предметом ложных срабатываний, если мы не следуем основным правилам. Чем выше уровень теста, тем больше вероятность ложных срабатываний. В интеграционных тестах выше вероятность ложных срабатываний из-за проблем с внешними ресурсами: нет подключения к Интернету, нет соединения с БД, случайная ошибка и многое другое.
Наша тестовая среда
Наша система «квази-наследие». Это не совсем наследство, потому что у него есть тесты. Эти тесты даже имеют хорошее покрытие. Это наследие из-за того, как он (не) структурирован и как построены тесты. Раньше его охватывали только интеграционные тесты. В последние несколько месяцев мы начали внедрять юнит-тесты. Особенно на новый код и новые функции.
Все наши интеграционные тесты наследуются от BaseTest , который наследует SpringJ AbstractJUnit4SpringContextTests . Контекст теста связывает все. Около 95% производственного кода. Это требует времени, но еще хуже, он подключается к реальным внешним ресурсам, таким как MongoDB и сервисам, которые подключаются к Интернету.
Чтобы повысить скорость тестирования, несколько недель назад я изменил MongoDB на встроенный. Это улучшило время выполнения тестов на порядок.
Этот тип настройки делает тестирование намного сложнее. Очень сложно издеваться над сервисами. Среда не изолирована от интернета и БД и многое другое.
После этого длинного вступления я хочу описать два проблемных теста и то, как я их исправил. Их общим недостатком было то, что они иногда терпели неудачу и обычно проходили мимо. Однако каждый потерпел неудачу по разной причине.
Пример 1 — Создание подключения к Интернету в конструкторе
В первом примере показан тест, который иногда не удался из-за проблем с соединением. Сложность заключалась в том, что в конструкторе был создан сервис. Этот сервис получил HttpClient, который также был создан в конструкторе.
Другая проблема заключалась в том, что я не мог изменить тест, чтобы использовать макеты вместо проводки Spring. Вот оригинальный конструктор (модифицированный для примера):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
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 .
Решение — извлечение и переопределение вызова
Идея была взята из Эффективной работы с Устаревшим Кодексом .
- Определить, что мне нужно исправить.
Чтобы сделать тест детерминированным и без реального подключения к интернету, мне нужен доступ для создания сервисов. - Я представлю защищенные методы, которые создают эти сервисы.
Вместо создания сервисов в конструкторе я буду вызывать эти методы. - В тестовой среде я создам класс, который расширяет тестируемый класс.
Этот класс переопределит эти методы и вернет поддельные (поддельные) сервисы.
Код решения
Тестируемый класс
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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(); } |
Тестовый класс
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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 — Статистические тесты для случайного ввода
Второй пример описывает тест, который не удался из-за случайных результатов и статистического утверждения. Код сделал случайный выбор между объектами с похожими атрибутами (здесь я упрощаю сценарий). Случайный объект был создан в конструкторе класса.
Упрощенный пример:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
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.
Код решения
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
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); } |
Вывод
Работа с устаревшим кодом может быть сложной и увлекательной. Работа с устаревшим тестовым кодом также может быть увлекательной. Очень приятно перестать получать раздражающие письма о неудачных тестах. Это также повышает доверие команды к процессу КИ.
Ссылка: | Работа с Legacy Test Code от нашего партнера JCG Эяля Голана в блоге « Обучение и совершенствование в качестве мастера» . |