Важная информация
RSS лента

Немного о java

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:
  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3. <modelVersion>4.0.0</modelVersion>
  4. <groupId>org.iforum.examples</groupId>
  5. <artifactId>unit-tests-sample</artifactId>
  6. <version>0.0.1-SNAPSHOT</version>
  7. <name>unit-tests-sample</name>
  8. <dependencies>
  9. <dependency>
  10. <groupId>junit</groupId>
  11. <artifactId>junit</artifactId>
  12. <version>4.11</version>
  13. </dependency>
  14. </dependencies>
  15. </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 converter = new NumberConverter();
Логично предположить, что в классе NumberConverter должен быть реализован метод который позволяет получить строковое представление числа.
Пусть этот метод будет назваться getStringValue();
Что бы получить строковое значение числа, это число сначала нужно передать в метод, поэтому делаем так:
Код :
String stringValue = converter.getStringValue(0);
Далее нам известно, что stringValue в результате выполнения функции должно иметь значение «ноль».
Что бы это проверить - воспользуемся методами которые для этого предоставляет jUnit.
Класс Assert - позволяет сравнивать ожидаемое значение с полученным.
Дописываем вот такой код:
Код :
Assert.assertEquals( "ноль", stringValue);
Вроде бы тест готов, но запустить мы его не можем, из-за существуют ошибки компиляции. А если посмотреть с точки зрения тестирования тест показывает, что отсутствует класс NumberConverter.
Надо его создать.
В 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.expect(Exception.class);
		exception.expectMessage("Not valid number");
		assertValue(42, null);
Так устанавливаются правила ожидания ошибки. Ожидаем, что исполнение метода приведет к вызову исключения типа Exception.class, и текст сообщения об ошибке будет “Not valid number”. Так же теперь сравниваем возвращаемое функцией значение с 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
Категории
Без категории

Комментарии