Статьи

Повторное выполнение тестов JUnit без циклов

Недавно я столкнулся с проблемой, когда мне пришлось писать тесты для метода, который вычисляет случайно распределенные значения в определенном диапазоне возможностей 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 выполняет свои функции, а функциональные возможности системы, основанные на упомянутой реализации, работают как очаровательные. Тем не менее, иногда кто-то скучает по лесу за деревьями, и поэтому я подумал, что было бы неплохо поделиться этим решением, чтобы увидеть, что придут другие люди.

  1. На самом деле это была только одна часть проблемной области, но я считаю это достаточной мотивацией для этого поста.
  2. Официально говорят: f (n, m) ∈ {e | e≥nm∧e≤n + m}, для всех e, n, m ∈ ℕ
  3. Короткий поиск в гугле нашел только похожее решение, доступное в Spring , которое не было доступно в моем наборе библиотек.

Ссылка: Запуск тестов JUnit несколько раз без циклов от нашего партнера JCG Фрэнка Аппеля в блоге Code Affine .