При написании 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