При написании unit тестов часто возникает необходимость проводить абсолютно идентичные испытания кода с разными данными. Примером такого кода может быть вычисление квадратного корня — для входных данных выше нуля мы ожидаем получить значения корня, а при передаче числа меньше 0, код должен возвращать ошибку (или, в редакции для эстетов, комплексное число). Очевидно, что для проверки этого поведения придётся написать два абсолютно идентичных теста, отличающихся только данными. Это настолько некрасиво, что хочется сразу с этим что-нибудь сделать.
В JUnit есть механизм, позволяющий отделить код теста от данных теста — параметризованные тесты.
Подготовка
Создадим пустой maven проект и добавим в него JUnit4:
1
2
3
4
5
|
>mvn archetype:generate -DgroupId=ru.easyjava.junit -DartifactId=parameters -Dversion=1 -DinteractiveMode=false
[INFO] Scanning for projects...
[INFO]
[...skipped...]
[INFO] BUILD SUCCESS
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<properties>
<junit.version>4.12</junit.version>
<hamcrest.version>1.3</hamcrest.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>${hamcrest.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
|
Класс для теста
Тестировать мы будем функцию по вычислению длины ортодромии. Вычисление расстояний между точками интересно тем, что имеет кучу особых условий — расстояние всегда должно быть кратчайшим, расстояние между двумя точками с одними координатами будет нулём, плюс должны корректно вычисляться расстояния между точками в разных полушариях: северном/южном, западном/восточном. Сама функция крайне проста:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/**
* Calculations on the great circle.
*/
public final class GreatCircle {
/**
* Length of a 1 degree of a meridian on the Earth.
*/
private static final double MERIDIAN_DEGREE_LEN = 111.12;
/**
* Calculates great circle distance between two points.
* @param latDeparture 1st point's latitude.
* @param lonDeparture 1st point's longitude.
* @param latDestination 2nd point's latitude.
* @param lonDestination 2nd point's longitude.
* @return Distance in meters.
*/
public static double distance(
final double latDeparture, final double lonDeparture,
final double latDestination, final double lonDestination
) {
double latDepartureRadians = Math.toRadians(latDeparture);
double lonDepartureRadians = Math.toRadians(lonDeparture);
double latDestinationRadians = Math.toRadians(latDestination);
double lonDestinationRadians = Math.toRadians(lonDestination);
return MERIDIAN_DEGREE_LEN
* Math.toDegrees(
Math.acos(
Math.sin(latDepartureRadians)
* Math.sin(latDestinationRadians)
+ Math.cos(latDepartureRadians)
* Math.cos(latDestinationRadians)
* Math.cos(lonDestinationRadians - lonDepartureRadians)); }
}
|
Параметризованный тест
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
@RunWith(Parameterized.class)
public class GreatCircleTest {
@Parameterized.Parameters(name = "{index}: Distance: {4}, Point 1: {0}/{1}, Point 2: {2}/{3}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
//{55.596111, 37.2675, 59.8002778, 30.2625, 861.624}, // Uncomment to see a error message
{55.596111, 37.2675, 59.8002778, 30.2625, 624.861}, // Vnukovo to Pulkovo distance
{55.596111, 37.2675, 55.596111, 37.2675, 0}, // Same point
{0, -180, 0, 180, 0}, // Same point
{-90, 0, 90, 0, 20001.6}, // Northern-Southern hemispheres
{0, 0, 0, 180, 20001.6} // Western-Eastern hemispheres
});
}
private double latDeparture;
private double lonDeparture;
private double latDestination;
private double lonDestination;
private double distance;
public GreatCircleTest(double latDeparture, double lonDeparture, double latDestination, double lonDestination, double distance) {
this.latDeparture = latDeparture;
this.lonDeparture = lonDeparture;
this.latDestination = latDestination;
this.lonDestination = lonDestination;
this.distance = distance;
}
@Test
public void testDistance() {
assertEquals(distance, GreatCircle.distance(latDeparture, lonDeparture, latDestination, lonDestination), 0.01);
}
}
|
Параметризованный тест — хороший пример использования Runners. Аннотация @RunWith(Parameterized.class) говорит нам, что тест будет исполняться с runner «Parameterized», который и реализует параметричекое тестирование и разрешает использовать конструкторы в классах тестов.
Сами параметры определяются в методе с аннотацией @Parameters:
1
2
3
4
5
6
7
8
9
10
11
|
@Parameterized.Parameters(name = "{index}: Distance: {4}, Point 1: {0}/{1}, Point 2: {2}/{3}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][]{
//{55.596111, 37.2675, 59.8002778, 30.2625, 861.624}, // Uncomment to see a error message
{55.596111, 37.2675, 59.8002778, 30.2625, 624.861}, // Vnukovo to Pulkovo distance
{55.596111, 37.2675, 55.596111, 37.2675, 0}, // Same point
{0, -180, 0, 180, 0}, // Same point
{-90, 0, 90, 0, 20001.6}, // Northern-Southern hemispheres
{0, 0, 0, 180, 20001.6} // Western-Eastern hemispheres
});
}
|
Метод возвращает Iterable, каждая запись которого представляет массив объектов. Одна запись — один набор данных для теста. В моём примере это таблица координат и расстояний, но это могут быть и любые, сколь угодно сложные, объекты. Аннотации @Parameters можно передать имя теста, для каждой итерации. В имени теста можно ссылаться на параметры по их индексам. Если имена заданы, то когда какой-либо тест проваливается, Вы сможете определить, с каким именно набором данных возникла проблема:
1
2
3
4
5
6
7
8
9
|
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running ru.morningjava.junit.GreatCircleTest
Tests run: 6, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.061 sec <<< FAILURE!
Results :
Failed tests: testDistance[0: Distance: 861.624, Point 1: 55.596/37.268, Point 2: 59.8/30.262](ru.morningjava.junit.GreatCircleTest): expected:<861.624> but was:<624.8616721136209>
|
Параметры передаются в конструктор, причём позиционно — нулевой элемент набора данных станет первым параметром конструктора итд. JUnit сам проверит количество и типы параметров и выкинет исключение, если они не будут cовпадать. Параметры для теста передаются один раз на итерацию, а не перед вызовом каждого теста, о чём нельзя забывать. Проще говоря — если у вас в классе несолько тестов, то каждый набор параметров будет установлен один раз, перед запуском всех тестов в классе, а не каждого текста и, таким образом, тестам не стоит изменять эти данные.
1
2
3
4
5
6
7
|
public GreatCircleTest(double latDeparture, double lonDeparture, double latDestination, double lonDestination, double distance) {
this.latDeparture = latDeparture;
this.lonDeparture = lonDeparture;
this.latDestination = latDestination;
this.lonDestination = lonDestination;
this.distance = distance;
}
|
Сам же тест у нас будет очень просто, про него и сказать нечего:
1
2
3
4
|
@Test
public void testDistance() {
assertEquals(distance, GreatCircle.distance(latDeparture, lonDeparture, latDestination, lonDestination), 0.01);
}
|
Исходный код примера доступен на github