Недавно я столкнулся с проблемой, когда мне пришлось писать тесты для метода, который вычисляет случайно распределенные значения в определенном диапазоне возможностей 1 . Точнее, если вы предполагаете, что подпись выглядит
1
2
3
|
interface RandomRangeValueCalculator { long calculateRangeValue( long center, long radius ); } |
тест может проверить следующее 2 :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
public class RandomRangeValueCalculatorImplTest { @Test public void testCalculateRangeValue() { long center = [...]; long radius = [...]; RangeValueCalculator calculator = [...]; long actual = calculator.calculateRangeValue( center, radius ); assertTrue( center + radius >= actual ); assertTrue( center - radius <= actual ); } } |
Однако вычисление значений диапазона для одного и того же центра и радиуса несколько раз даст разные результаты (по крайней мере, большую часть времени). Поэтому решение казалось несколько хрупким в том смысле, что плохая реализация может легко привести к периодическим сбоям. С другой стороны, я не хотел погружаться в глубины фактического искажения распределения значений. Последнее (случайное, гауссовское или подобное) было предоставлено соавтором, и его правильное использование уже было подтверждено дополнительными тестами.
Мне пришло в голову, что более прагматичным решением было бы на самом деле запускать вышеописанный тест автоматически снова и снова, чтобы сделать его более «значимым». Конечно, самым простым способом добиться этого было бы зациклить содержание теста и продолжать жить.
Но для начала это выглядело несколько неправильно, когда утверждения были в цикле и смешивали два аспекта в одном тестовом прогоне. И, что еще важнее, рассматриваемая проблемная область требует большего количества подобных тестов. Поэтому, учитывая намерение сократить избыточность, я вспомнил свой пост о JUnit-Rules и реализовал простое правило повтора 3 . С этим правилом, вышеприведенный тест можно было бы мягко изменить:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
public class RandomRangeValueCalculatorImplTest { @Rule public RepeatRule repeatRule = new RepeatRule(); @Test @Repeat ( times = 10000 ) public void testCalculateRangeValue() { long center = [...]; long radius = [...]; RangeValueCalculator calculator = [...]; long actual= calculator.calculateRangeValue( center, radius ); assertTrue( center + radius >= actual ); assertTrue( center - radius <= actual ); } } |
Я думаю, что довольно легко понять, что метод testCalculateRangeValue
будет выполняться 10000 раз при выполнении тестового примера. Следующий фрагмент кода демонстрирует реализацию RepeatRule, которая является прямой:
01
02
03
04
05
06
07
08
09
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
36
37
38
39
40
41
|
public class RepeatRule implements TestRule { @Retention ( RetentionPolicy.RUNTIME ) @Target ( { java.lang.annotation.ElementType.METHOD } ) public @interface Repeat { public abstract int times(); } private static class RepeatStatement extends Statement { private final int times; private final Statement statement; private RepeatStatement( int times, Statement statement ) { this .times = times; this .statement = statement; } @Override public void evaluate() throws Throwable { for ( int i = 0 ; i < times; i++ ) { statement.evaluate(); } } } @Override public Statement apply( Statement statement, Description description ) { Statement result = statement; Repeat repeat = description.getAnnotation( Repeat. class ); if ( repeat != null ) { int times = repeat.times(); result = new RepeatStatement( times, statement ); } return result; } } |
До сих пор RepeatRule выполняет свои функции, а функциональные возможности системы, основанные на упомянутой реализации, работают как очаровательные. Тем не менее, иногда кто-то скучает по лесу за деревьями, и поэтому я подумал, что было бы неплохо поделиться этим решением, чтобы увидеть, что придут другие люди.
- На самом деле это была только одна часть проблемной области, но я считаю это достаточной мотивацией для этого поста. ↩
- Официально говорят: f (n, m) ∈ {e | e≥nm∧e≤n + m}, для всех e, n, m ∈ ℕ ↩
- Короткий поиск в гугле нашел только похожее решение, доступное в Spring , которое не было доступно в моем наборе библиотек. ↩