Когда делаешь первые попытки писать юнит-тесты обычно обычно сталкиваешься с проблемой начинания: вроде бы документация прочитана, цель ясна, а с чего начинать — не понятно.
Попробуем вместе написать простой юнит-тест, для более-менее настоящего класса, в котором испытаем почти весь базовый функционал JUnit.
Подготовка
Создадим пустой maven проект:
>mvn archetype:generate -DgroupId=ru.easyjava.junit -DartifactId=base -Dversion=1 -DinteractiveMode=false [INFO] Scanning for projects... [...skipped...] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------
И добавим в него JUnit:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <junit.version>4.12</junit.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies>
Зависимость добавлена с <scope>test</scope> , что говорит maven, что она требуется только при сборке и исполнении тестов.
Класс для тестирования
Поскольку я обещал почти настоящий пример, придётся написать хоть сколько-то полезный класс. Пускай это будем набор утилит для работы со строками:
- Разделение строки на подстроки
- Слияние строк из массива
- Проверка строк на пустоту
- Преобразование в число и обратно
public final class StringUtils { /** * Do not construct me. */ private StringUtils() { } /** * Combines array of string to the single string, * inserting delimiters between array entries. * * @param source Array of string to join. * @param del Delimiter for array entries. * @return null if array is null or joined array entries. */ public static String joinArray(final String[] source, final char del) { if (source == null) { return null; } StringBuilder result = new StringBuilder(); for (int i = 0; i < source.length - 1; i++) { result.append(source[i]); result.append(del); } result.append(source[source.length - 1]); return result.toString(); } /** * Splits the supplied strings to array of * strings. * @param source String to split. * @param delimiter Character to use as token boundary. * @return empty array if source is null or array of substrings, split * on delimiter character. */ public static String[] toArray(final String source, final char delimiter) { if (source == null) { return new String[]{}; } return source.split(Character.toString(delimiter)); } /** * Checkes whether string contains any usable * content (any non-empty characters). * @param subject String to test. * @return true if string doesn't have any contents or * have only white space characters in it, false otherwise. */ public static boolean isEmpty(final String subject) { return subject == null || subject.replaceAll("\\s", "").isEmpty(); } /** * Tries to extract double value from String. * @param source String to process. * @return extracted double value or NaN if * source is null. */ public static double toDouble(final String source) { if (source == null) { return Double.NaN; } return Double.valueOf(source); } /** * Converts double to string. * @param source value to convert. * @return Textual representation of double. */ public static String fromDouble(final double source) { return String.valueOf(source); } }
Не самый полезный набор утилит, но всё же ????
Тесты
Тесты в JUnit располагаются в отдельных классах, методы которых, имеющие аннотацию @Test, и возвращающие void, и есть сами тесты. Имя класса может быть в принципе любое, но рекомендуется придерживаться шаблона ИмяТестируемогоКлассаTest , так как это упрощает чтение кода. К тому же обычно средства автоматического запуска тестов, такие как плагин maven maven—surefire—plugin предполагают, что классы с юнит-тестами оканчиваются на *Test
public class StringUtilsTest { @Test public void testToArray() { } @Test public void testJoinArray() { } @Test public void testIsEmpty() { } @Test public void testToDouble() { } @Test public void testFromDouble() { } }
Maven традиционно располагает тесты в каталоге src/test , в то время как основной исходный код располагается в src/main . Разумеется это всего лишь договорённость, используемая в maven по умолчанию, и тесты и код можно располагать любым удобным образом.
Название тестовых методов так же могут быть любыми, однако для повышения читаемости кода, рекомендуется начинать их с префикса test* и отражать в названии суть теста.
Первый юнит-тест
@Test public void testFromDouble() { double source = 3.1415; String expected="3.1415"; String actual = StringUtils.fromDouble(source); assertEquals("Unexpected string value", expected, actual); }
Все юнит-тесты пишутся по единому шаблону: создаются входные данные, создаются эталонные данные (expected), вызывается тестируемый код и
результат его работы(actual) сравнивается с эталонными данными. JUnit предоставляет несколько assert* функций, выполняюших сравнение.
В первом юнит-тесте строка
double source = 3.1415;
и есть входные данные, которые мы отдаём в проверяемую функцию.
Эталонные данные определены в следующей строке:
String expected="3.1415";
Вызываем проверяемый код и сохраняем результат его работы:
String actual = StringUtils.fromDouble(source);
Наконец самая главная часть теста, проверка:
assertEquals("Unexpected string value", expected, actual);
assertEquals сравнивает эквивалентность объектов expected и actual и, в случае когда они не эквивалентны, проваливает тест и выводит сообщение «Unexpected string value». Функции assert* можно использовать и без сообщения:
assertEquals(expected, actual);
Однако с сообщением результаты тестирования становятся гораздо приятнее при чтении.
Разработчики обычно пишут юнит-тесты только для предусмотренных разработчиком/архитектором/документацией/etc вариантов поведения функции. Для
StringUtils.fromDouble() документация указывает что функция должна преобразовать цисло с плавающей запятой в строку.
Юнит-тест этой функции покрывает только описанный функционал. Цель юнит-тестирования — убедиться, что функция работает правильно, а не искать условия,
в которых она работает неправильно.
Более того, сам юнит-тест уже является краткой и понятной документацией к функции. В четырёх строках чётко и однозначно написано, как ведёт себя функция: возвращает новый строковый объект, значение которого является переданным ей числом с плавающей запятой, записанное в десятичной системе счисления.
А самый главный бонус юнит-тестирования, это фиксация поведения кода. Вы знаете, прямо сейчас, что функция ведёт себя определённым образом. И код, который её использует, полагается на это поведение. Если Когда вы захотите изменить эту функцию, юнит-тест будет вам гарантировать, что
поведение функции осталось таким же (либо тест провалится). Следовательно остальной код не заметит изменения реализации функции, а это значит, что с этого момента вы можете спокойной менять любую часть кода: юнит-тесты не позволят вам что-нибудь сломать.
Второй юнит-тест
Следующий юнит-тест напишем для обратной функции преобразования строки в число с плавающей запятой:
@Test public void testToDouble() { assertEquals(3.1415, StringUtils.toDouble("3.1415"), 0.0001); assertEquals("Not NaN for null", Double.NaN, StringUtils.toDouble(null), 0.00001); }
Это очень простая функция и поэтому тест тоже очень простой: входное значение, эталонное значение и сравнение с результатом.Обычно при написании таких простых тестов явно не заводят переменные для значений, а пишут их прямо в assert* функции.
Однако у assertEquals в этом тесте появился дополнительный параметр! Дело в том, что сравнивать числа с плавающей запятой непосредственно друг с другом нельзя, так как они не имеют точного двоичного представления. Обычно числа сравниваются с некоторой погрешностью: можно сказать что 3.1415000000001 эквивалентно
3.1415000000002 с погрешностью до 0.000000000001. И именно эта погрешность передаётся в третий параметр assertEquals для числе с плавающей запятой. Вторая часть теста очевидна — проверяется что для переданного null возвращается NaN.
Третий юнит-тест
@Test public void testIsEmpty() { assertFalse("Non empty string claimed to be empty", StringUtils.isEmpty("TEST")); assertTrue("Empty string not recognized", StringUtils.isEmpty("")); assertTrue("Whitespaces not recognized",StringUtils.isEmpty(" ")); }
Теперь наоборот — у функций assertTrue и assertFalse не хватает одного аргумента. А всё потому, что эти функции проверяют только логические значения (первая ожидает true, вторая, соответственно, false ), которые и не с чем сравнивать. В самом же тесте проверяется документированние поведение: строка с значением очевидно не пуста, строка без каких-либо символов в ней — пуста и, наконец, строка с невидимыми символами тоже признаётся пустой.
Четвёртый и пятый юнит-тесты
@Test public void testToArray() { String[] expected = {"T", "E", "S", "T"}; String source="T:E:S:T"; assertArrayEquals("Wrong array", expected, StringUtils.toArray(source, ':')); assertNull(StringUtils.toArray(null, ':')); }
В последних двух тестах нам придётся работать с массивами. Массивы нельзя проверить через assertEquals, так как для массивов assertEquals ведёт себя как assertSame:
assertEquals(expected, StringUtils.toArray(source, ':')); junit.framework.AssertionFailedError: expected:<[Ljava.lang.String;@61b383e9> but was:<[Ljava.lang.String;@5099681b>
Поэтому в JUnit предусмотрена специальная функция для сравнения массивов assertArrayEquals, которая сравнивает эквивалентность каждого элемента обоих массивов друг с другом. Разумеется сравниваются между собой элементы с одинаковой позицией в массиве и массивы разной длины сразу признаются не эквивалентными. Надо отметить что обратной функции для assertArrayEquals не предусмотрено.
Тест для последней оставшейся функции (joinArray) я предлагаю написать самостоятельно.
Пример теста, для сравнения:
@Test public void testToArray() { String[] expected = {"T", "E", "S", "T"}; String source="T:E:S:T"; assertArrayEquals("Wrong array", expected, StringUtils.toArray(source, ':')); assertEquals(0,StringUtils.toArray(null, ':').length); }
Исполнение тестов
Проще всего использовать для этого maven:
>mvn test [INFO] Scanning for projects... [...skipped...] [INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ base --- ------------------------------------------------------- T E S T S ------------------------------------------------------- Running ru.easyjava.junit.StringUtilsTest Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.066 sec Results : Tests run: 5, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS
Но при необходимости можно и вручную:
Z:\Dropbox\Work\MorningJava\Blog\Testing\JUnit\Base\manual>java -cp .;junit-4.12.jar;hamcrest-core-1.3.jar org.junit.runner.JUnitCore ru.easyjava.junit.StringUtilsTest JUnit version 4.12 ..... Time: 0,02 OK (5 tests)
Нужно всего лишь вручную указать правильный classpath, включающий в себя junit с зависимостями и ваши классы ????