Unit-tests Часть 2, Практическая.
Что нам потребуется:
1. Среда разработки — Spring Tools Suit. Скачать можно тут http://spring.io/tools/sts
Собственно все.
Что сделаем.
1. Создадим новый проект.
2. Добавим к проекту нужные нам библиотеки.
3. Напишем тесты.
4. Напишем программу.
Цель.
Описать последовательность действий при создании юнит-теста. Пример использования jUnit для организации модульного тестирования.
Создание проекта.
Создание проекта
Приступаем к пункту один. Создаем новый проект. File → New->Other и ищем Maven Project. В диалоговом окне выставляем галочку — Create Simple Project. Жмем далее и заполняем форму.
Group Id – org.iforum.examples
Artifact Id - unit-tests-sample
Так же указываем имя
Name – unit-tests-sample
Нажимаешь кнопку finish. И наш тестовый проект создан.
По умолчанию в проекте будет создано два месторасположения для исходных кодов:
1. src/main/java – Тут должны быть исходники программы.
2. src/test/java – Тут исходники тестов.
Немножко поясню ситуацию. В данном проекте мы используем такой инструмент как Maven для организации структуры проекта, сборки, тестирования и управления зависимостями. Как пользоваться Maven, что это, и какие возможности предоставляет — предмет отдельной статьи и сейчас подробно рассматриваться не будет.
Добавление jUnit к проекту.
Следующий шаг - добавить к проекту библиотеку с помощью которой мы организуем unit тестирование. Самый обычный и самый стандартный выбор – junit.
Что бы добавить библиотеку открываем файл pom.xml(он в корне проекта). Затем переходим на его xml представление (в открывшемся окне редактирования выбираем вкладку pom.xml).
Данный файл описывает проект, а так же может включать информацию о том как проект должен собираться, какие плагины должны быть использованы при сборке. Но нам важно, что тут можно указать от каких библиотек зависим проект. В нашем случае нам необходима библиотека junit.
Что бы ее добавить — найдем в интернете страничку проекта в репозитории maven - http://mvnrepository.com/artifact/junit/junit/4.11 и скопируем объявление зависимости и вставим его в тэг dependencies в pom.xml. После этого pom.xml должен выглядеть примерно так:
HTML Code:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.iforum.examples</groupId> <artifactId>unit-tests-sample</artifactId> <version>0.0.1-SNAPSHOT</version> <name>unit-tests-sample</name> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> </dependency> </dependencies> </project>
Сохраним изменения и библиотека будет автоматически загружена из интернета и добавлена в наш проект.
Первый тест
Сперва определимся с задачей которую будет выполнять программа.
Задача — написать сервис который будет преобразовывать числа от 0 до 9 в строковое представление. То есть 0 в ноль, 1в один … 9 в девять.
И так начинается самое интересное. Первым делом мы создаем тест сервиса, а не сам сервис.Добавляем в src/test/java новый package с названием org.iforum.examples.test. В этот пакет добавляем новый класс NumberConverterTest. В классе создаем метод getStringValueTest, и помечаем его аннотацией @Test. Данная аннотация служит маркером для jUnit. Фактически при прохождении фазы тестирования, фреймворк, находя метод с данной аннотацией считает его тестом и выполняет его запуск. Что нам и нужно.
Ради интереса можно попробовать запустить тест. Правой кнопкой мыши на проекте — runs as → run as jUnit tests. После этого отобразиться окно с информацией о том какие тесты были пройдены и результат их выполнения. Наш тест пустой, поэтому он будет считаться успешно выполненным.
Далее, делаем предположение — для того что бы реализовать поставленную задачу, нужно будет создать класс и пусть он называется — NumberConverter. Физически его создавать пока не будем, он пока существует только в теории.
В тесте объявляем переменную :
Логично предположить, что в классе NumberConverter должен быть реализован метод который позволяет получить строковое представление числа.Код :NumberConverter converter = new NumberConverter();
Пусть этот метод будет назваться getStringValue();
Что бы получить строковое значение числа, это число сначала нужно передать в метод, поэтому делаем так:
Далее нам известно, что stringValue в результате выполнения функции должно иметь значение «ноль».Код :String stringValue = converter.getStringValue(0);
Что бы это проверить - воспользуемся методами которые для этого предоставляет jUnit.
Класс Assert - позволяет сравнивать ожидаемое значение с полученным.
Дописываем вот такой код:
Вроде бы тест готов, но запустить мы его не можем, из-за существуют ошибки компиляции. А если посмотреть с точки зрения тестирования тест показывает, что отсутствует класс NumberConverter.Код :Assert.assertEquals( "ноль", stringValue);
Надо его создать.
В src/main/java создаем package org.iforum.examples и добавляем в него класс NumberConverter. Сохраняем, добавляем импорт класса в тесте.
Теперь тест показывает отсутствие метода getStringValue в NumberConverter.
Создаем соотвествующий метод в классе.
public String getStringValue(int number){
return null;
}
Больше ошибок компиляции нет. Запускаем тест и получаем следующею ошибку: java.lang.AssertionError: expected:<ноль> but was:<null>.
Делаем вывод, что функция возвращает не правильное значение и его надо модифицировать. Что бы удовлетворить условие теста функция должна возвращать значение «ноль». Поэтому return null, меняем на return "ноль".
Запускаем тест и видим, что тест проходит успешно. Но мы проверили лишь одно условие, надо проверить и остальные.Код :public String getStringValue(int number){ return "ноль"; }
Поэтому добавляем в тест код проверки остальных условий. Что бы не писать много кода, сделаем небольшой рефакторинг нашего теста.
1. Сделаем переменную converter полем класса.
2. И добавим в класс приватную функцию assertValue();
После преобразований код будет выглядеть так:
[CODE]
public class NumberConverterTest {
private NumberConverter converter = new NumberConverter();
@Test
public void getStringValueTest() {
assertValue(0,"ноль");
}
private void assertValue(int inValue, String expected){
String stringValue = converter.getStringValue(inValue);
Assert.assertEquals(expected,stringValue);
}
}
[CODE]
Добавляем тест проверку всех значений от 0 до 9-ти. Пробуем запустить тест и естественно получаем ошибку сравнения. Необходимо модифицировать метод getStringValue, так, что бы он удовлетворял всем условиям теста.Я очень не люблю писать длинные if и switch.
После доработки класс стал выглядеть так:
Запускаем тест. И он успешно проходит. Ура все работает!! А все ли??Код :public class NumberConverter { private final Map<Integer,String> convertData; public NumberConverter(){ convertData = new HashMap<Integer,String>(); convertData.put(0,"ноль"); convertData.put(1,"один"); convertData.put(2,"два"); convertData.put(3,"три"); convertData.put(4,"четыре"); convertData.put(5,"пять"); convertData.put(6,"шесть"); convertData.put(7,"семь"); convertData.put(8,"восемь"); convertData.put(9,"девять"); } public String getStringValue(int number) { return convertData.get(number); } }
Например что будет если в метод передать 42?
Для этого создадим ещ один метод testUnexpectedValues().
И попробуем вызвать там assertValue(42,”Сорок два”).
Получим проваленный тест: java.lang.AssertionError: expected:<сорок два> but was:<null>.
Конечно, можно в такой ситуация ожидать и возвращение null, в качестве ответа на невалидные данные. Но поскольку в условии задачи не сказано, что нужно возвращать null, разумней будет кидать Exception если данные невалидны. В будущем, при использовании метода в коде, это даст нам возможность явно узнать, что передаются данные которые не должны были попасть в функцию. То есть, таким способом мы откладываем решение по обработке возвращаемого значения на код который будет использовать метод getStringValue.
Теперь стоит задача проверить, что при вызове getStringValue() с невалидными данными будет вызвано исключение. Это можно сделать двумя способами.
1) С помощью try catch.
2) С помощью аннотации феймворка jUnit @Rule.
Второй вариант мне нравиться больше. Делаем следующие действия. Объявляем тестовом классе новое поле.
Это правило — правило ожидания ошибок, оно будет применяться к каждому вызову ванному тесту. По умолчания мы задет значение — что ошибок быть не должно.Код :@Rule public ExpectedException exception = ExpectedException.none();
Затем вносим изменения в testUnexpectedValues().
Так устанавливаются правила ожидания ошибки. Ожидаем, что исполнение метода приведет к вызову исключения типа Exception.class, и текст сообщения об ошибке будет “Not valid number”. Так же теперь сравниваем возвращаемое функцией значение с null. Это нужно, что бы убрать сообщение о том, что сравнение значений провалено, в случае если метод не кидает исключение.Код :exception.expect(Exception.class); exception.expectMessage("Not valid number"); assertValue(42, null);
Запускаем тест— получаем ожидаемую ошибку:
java.lang.AssertionError: Expected test to throw (an instance of java.lang.Exception and exception with message a string containing "Not valid number")
Теперь модифицируем метод так, что бы он в случае невалидных параметров вызвал исключение. Выглядеть он убдет так:
Запускаем тестирование, все тесты проходят на отлично.Код :public String getStringValue(int number) throws Exception { String result = convertData.get(number); if (result == null) { throw new Exception("Not valid number"); } return result; }
Теперь осталось навести в коде порядок.
1. У нас есть дублирующая строка “Not valid number”
2. Хотелось бы знать, какие именно данные вызвали исключение при выполнении метода.
Строку надо вынести в константу. Для этого создадим специальный класс TestSampleMessage куда добавим константу NOT_VALID_NUMBER_MESSAGE
Код :public class TestSampleMessage { public static final String NOT_VALID_NUMBER ="Not valid number, expect value is between 0 and 9 but was %s"; }
И заменим в тесте и в сервисе сроку на это сообщение, так что бы она включала в себя информацию о параметрах вызова метода.
Код :@Test public void testUnexpectedValues() throws Exception { int testNumber = 42; exception.expect(Exception.class); exception.expectMessage(String.format(TestSampleMessage.NOT_VALID_NUMBER, testNumber)); assertValue(testNumber, null); } public String getStringValue(int number) throws Exception { String result = convertData.get(number); if (result == null) { throw new Exception(String.format(TestSampleMessage.NOT_VALID_NUMBER, number)); } return result; }
В заключении хочется сказать, что всегда есть соблазн написать тесты абы как, не очень заботясь о структуре кода. Обычно в последствии это выходит боком и тесты становиться не возможно поддерживать в актуальном виде. Тесты тоже нужно рефакторить, и оптимизировать. И самое главное следить за их актуальностью, а еще лучше сначала вносить изменения в нужный тест, а потом только в код.
Исходные коды примера можно взять тут: https://github.com/inngvar/iform-examples