Главная » Хабрахабр » Как писать юнит-тесты, если совсем не хочется

Как писать юнит-тесты, если совсем не хочется

Многие уже поняли, что от них один вред. Всех нас на работе то и дело пытаются заставить писать юнит-тесты. Если тест неожиданно начинает падать, ломается сборка на сервере непрерывной интеграции, не выкатывается вовремя релиз, бизнес теряет деньги и крайним оказываетесь вы, автор упавшего юнит-теста. Написание тестов отнимает много времени, за которое вы могли бы сделать что-то более полезное. При рефакторинге тесты причиняют головную боль, потому что начинают падать и приходится с этим разбираться.

Особо хитрые менеджеры даже считают покрытие и не отпускают вас с работы, пока оно не будет достигнуто. Тем не менее злые начальники требуют больше тестов, говоря о так называемом «контроле качества». Сплошное расстройство! Ваш код заворачивают на ревью, если в нём нет тестов или они чем-то не понравились.

Что же делать?

Эти способы придумал не я, их успешно практикуют в ряде опенсорсных проектов. К счастью, есть способы писать надёжные юнит-тесты, которые никогда не упадут. Поэтому нет причин и вам не воспользоваться тем, что уже применяется на практике другими разработчиками! Все примеры, которые я приведу, взяты из реального кода.

Вот простой пример: Самый первый и очевидный способ: ничего не проверять в юнит-тесте.

public void testSetFile() { System.out.println("setFile"); File f = null; BlastXMLParser instance = new BlastXMLParser(); instance.setFile(f);
}

Отлично, протестируем, что пустой конструктор по умолчанию и тривиальный сеттер не падают с исключением. Начальник требует стопроцентного покрытия? Это надёжно, такой тест падать не должен. То что сеттер действительно что-то установил проверять не будем, тем более что по факту мы null перезаписали null'ом.

Можно поступить хитрее: Слишком банально и не удаётся такое пропихнуть на ревью?

@Test
public void getParametersTest() { List<IGeneratorParameter<?>> parameters = generator.getParameters(); containsParameterType(parameters, AtomColor.class); containsParameterType(parameters, AtomColorer.class); containsParameterType(parameters, AtomRadius.class); containsParameterType(parameters, ColorByType.class); ...
}

Взглянем однако на метод containsParameterType: Выглядит солидно, будто что-то действительно проверяется.

public <T> boolean containsParameterType(List<IGeneratorParameter<?>> list, Class<T> type) return false;
}

Оба метода по отдельности выглядят разумно, но вместе они ничего не проверяют. Изящно, правда? Тем более что они в разных классах! Такое может прокатить, особенно если методы закоммитить по отдельности и отправить разным ревьюверам.

Злое начальство заподозрит неладное, не увидев никаких ассертов в коде. Однако так долго не протянешь. Но, например, так, чтобы они не выполнялись. Ассерты всё-таки добавлять стоит. Вот грубый подход:

for (int i = 0; i < 0; i++)
{ Assert.assertTrue(errorProbabilities[i] > 0.0d);
}

Такое пропустит разве что сильно пьяный ревьювер. Цикл на 0 итераций. Однако следующий вариант гораздо изящнее:

List<JavaOperationSignature> sigs = new ArrayList<>();
List<JavaOperationSignature> sigs2 = new ArrayList<>(); for (int i = 0; i < sigs.size(); i++) { // делаем вид, что заполняем списки sigs.add(JavaOperationSignature.buildFor(nodes.get(i))); sigs2.add(JavaOperationSignature.buildFor(nodes2.get(i)));
} for (int i = 0; i < sigs.size() - 1; i++) { // делаем вид, что сравниваем assertTrue(sigs.get(i) == sigs.get(i + 1)); assertTrue(sigs2.get(i) == sigs2.get(i + 1));
}

Оба цикла ни разу не выполняются, потому что граница — размер пустого списка. Тут уже многие ревьюверы не заметят подвоха! Берите на заметку.

Сделал он это старомодным способом, через catch. Предположим, ваш коллега десять лет назад написал тест, проверяющий, что метод кидает исключение. Никто не заметит, что они не выполняются: Прекрасно, допишите любых новых ассертов в конец метода.

try { getDs().save(e);
} catch (Exception ex) { return; // нормальный выход из теста здесь!
} // Следующая строчка выполнится, если что-то пойдёт не так
Assert.assertFalse("Should have got rejection for dot in field names", true); // А это не выполнится никогда
e = getDs().get(e);
Assert.assertEquals("a", e.mymap.get("a.b")); // Но никто этого не заметит!
Assert.assertEquals("b", e.mymap.get("c.e.g"));

Теперь они замечают такие штуки? Ваши менеджеры совсем обнаглели и смотрят на покрытие не только основного кода, но и тестов? Будем писать ассерты, которые проверяют всякую ерунду. Ладно, и с этим можно бороться. Например, что свежесозданный объект не равен null:

Assert.assertNotNull(new Electronegativity());

Поэтому такой ассерт надёжен как скала. Если оператор new у вас возвращает null, то у вашей виртуальной машины серьёзные проблемы. Более хитрый способ обмануть систему — проверить булево значение: Хотя, конечно, опытному ревьюверу сразу бросится в глаза.

DocumentImplementation document = new DocumentImplementation(props);
assertNotNull(document.toString().contains(KEY));
assertNotNull(document.toString().contains(VALUE));

Этот ассерт уже не так бросается в глаза, честно выполняется и ему совершенно наплевать, true там вернётся или false. Благодаря автобоксингу примитивный boolean заворачивается в объектный Boolean, который, конечно, никогда не будет нуллом. Подобный фокус работает и с другими примитивными типами:

Assert.assertNotNull("could not get nr. of eqr: ", afpChain.getNrEQR());

Ревьюверу придётся проявить истинную дотошность, чтобы заметить, что метод getNrEQR возвращает примитивный int и поэтому такой ассерт не может упасть. В отрыве от контекста код выглядит вполне разумно.

Ещё отличный способ ничего не проверить — написать длинное сообщение к ассерту с конкатенацией разных компонентов, а второй аргумент вообще убрать:

Assert.assertNotNull("Attempt to test atom type which is not defined in the " + getAtomTypeListName() + ": " + exception.getMessage());

Кажется, раз у нас длинное-длинное сообщение, то проверяется что-то серьёзное. Видите? На самом деле проверяется, что это самое сообщение не равно null, чего не может быть, потому что конкатенация строк в джаве всегда выдаст ненулевой объект.

Но не единственный друг! Вообще, конечно, если хочется бросить пыль в глаза, то assertNotNull ваш лучший друг. К примеру, assertEquals прекрасно можно использовать, чтобы сравнивать число с самим собой:

Assert.assertEquals(ac2.getAtomCount(), ac2.getAtomCount());

Может кто-нибудь вместо простого геттера туда генератор случайных чисел засунет! А если вас поймают за руку, всегда можно оправдаться, что вы проверяли стабильность метода getAtomCount.

К сожалению, assertNotEquals тут вам не помощник, он слишком умный. Если вы работаете с типом double, то самое время вспомнить про сравнение с NaN. Но всегда можно использовать assertTrue:

Assert.assertTrue(result1.get(i) != Double.NaN);

Как известно, NaN ничему не равен, даже самому себе, а потому такое сравнение всегда истинно.

Также assertTrue полезен для очевидных instanceof-проверок:

Assert.assertNotNull(cf.getRealFormat());
Assert.assertNotNull(cf.getImaginaryFormat());
Assert.assertTrue(cf.getRealFormat() instanceof NumberFormat);
Assert.assertTrue(cf.getImaginaryFormat() instanceof NumberFormat);

Но на нулл мы и так проверили выше. Методы getRealFormat и getImaginaryFormat и так возвращают NumberFormat, так что instanceof проверяет разве что неравенство нуллу. Таким нехитрым образом число ассертов можно увеличить вдвое.

Скажем, можно использовать метод assertThat из AssertJ и не воспользоваться результатом (например, assertThat(somethingIsTrue()) вместо assertThat(somethingIsTrue()).is(true)). Есть ещё ряд способов, по которым я быстро не нашёл примеров. } catch(Throwable t) {} так чтобы поймать и проигнорировать AssertionError. Можно завернуть текст теста в большой try { ... Например, отправить ассерты в фоновый поток через CompletableFuture.runAsync() и не сджойнить результат. Такого же эффекта можно добиться хитрее. Я уверен, вы и сами придумаете множество способов, чтобы противостоять менеджерскому произволу.

Начальство ни в коем случае не должно узнать про статический анализ. Но будьте осторожны. Большинство упомянутых в статье способов, к сожалению, легко находятся хорошим статическим анализатором. Иначе всё зря. Так что т-с-с-с!


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

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Из песочницы] Haiku β1 — сделаем /b/ OS великой снова

Совсем недавно (почти 4 месяца назад) вышла новая Haiku (далее — просто BeOS, ибо проект гораздо удачнее ReactOS — настолько, что разница между Haiku и BeOS уже пренебрежимо мала). Да и недавно прочитанный киберпанк-роман Александра Чубарьяна давал понять, что BeOS ...

Минкомсвязи одобрило законопроект об изоляции рунета

Министерство цифрового развития, связи и массовых коммуникаций РФ поддержало законопроект №608767-7 об автономной работе рунета, внесённый в Госдуму 14 декабря 2018 года. Об этом сегодня сообщил замглавы Минкомсвязи Олег Иванов в ходе расширенного заседания комитета Госдумы по информационной политике, информационным технологиям ...