Статьи

Введение в теории юнитов

Вы когда-нибудь читали математическую теорию?

Обычно это выглядит примерно так:

Для всех a, b> 0 верно следующее: a + b> a и a + b> b

Просто заявления обычно сложнее понять.

В таком утверждении есть кое-что интересное: оно справедливо для КАЖДОГО элемента (или комбинации элементов) довольно большого (в данном случае, бесконечного) набора.

Сравните это с утверждением типичного теста:

1
2
3
4
5
6
7
@Test
 public void a_plus_b_is_greater_than_a_and_greater_than_b(){
   int a = 2;
   int b = 3;
   assertTrue(a + b > a);
   assertTrue(a + b > b);
 }

Это просто утверждение об одном элементе большого набора, о котором мы говорили. Не очень впечатляет. Конечно, мы можем это исправить, зациклив тест (или используя параметризованные тесты ):

1
2
3
4
5
6
7
8
9
@Test
 public void a_plus_b_is_greater_than_a_and_greater_than_b_multiple_values() {
    List<Integer> values = Arrays.asList(1, 2, 300, 400000);
    for (Integer a : values)
      for (Integer b : values) {
         assertTrue(a + b > a);
         assertTrue(a + b > b);
      }
    }

Конечно, это все еще только тестирует несколько значений, но это также стало довольно уродливо. Мы используем 9 строк кода, чтобы проверить, что математик пишет в одной строке! И главное, что это отношение должно выполняться для любого значения a, b, полностью теряется в переводе.

Но есть надежда: Юнит Теории . Давайте посмотрим, как выглядит тест с этим отличным инструментом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;
 
import static org.junit.Assert.assertTrue;
 
@RunWith(Theories.class)
public class AdditionWithTheoriesTest {
 
  @DataPoints
  public static int[] positiveIntegers() {
       return new int[]{
                        1, 10, 1234567};
  }
 
  @Theory
  public void a_plus_b_is_greater_than_a_and_greater_than_b(Integer a, Integer b) {
      assertTrue(a + b > a);
      assertTrue(a + b > b);
  }
}

В JUnit Theories тест разделяется на две отдельные части: метод, предоставляющий точки данных, т.е. значения, которые будут использоваться для тестов, и саму теорию. Теория выглядит почти как тест, но имеет другую аннотацию (@Theory) и принимает параметры. Теории в классе выполняются с каждой возможной комбинацией точек данных.

Это означает, что если у нас есть более одной теории о нашем объекте испытаний, мы должны объявить точки данных только один раз. Итак, давайте добавим следующую теорию, которая должна быть верна для сложения: a + b = b + a Итак, мы добавим следующую теорию к нашему классу б + а); }

Это работает как талисман, и можно начать видеть, что на самом деле это также сохраняет некоторый код, потому что мы не дублируем точки данных. Но мы тестируем только с положительными целыми числами, в то время как коммутативное свойство должно выполняться для всех целых чисел! Конечно, наша первая теория все еще верна только для положительных чисел

Для этого также есть решение: предположим . Предположим, вы можете проверить предварительные условия для вашей теории. Если это не так для данного набора параметров, теория будет пропущена для этого набора параметров. Итак, наш тест теперь выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@RunWith(Theories.class)
 public class AdditionWithTheoriesTest {
 
  @DataPoints
  public static int[] integers() {
     return new int[]{
                   -1, -10, -1234567,1, 10, 1234567};
  }
 
  @Theory
  public void a_plus_b_is_greater_than_a_and_greater_than_b(Integer a, Integer b) {
     Assume.assumeTrue(a >0 && b > 0 );
     assertTrue(a + b > a);
     assertTrue(a + b > b);
  }
 
  @Theory
  public void addition_is_commutative(Integer a, Integer b) {
     assertTrue(a + b == b + a);
  }
}

Это делает тесты очень выразительными.

Отделение тестовых данных от реализации теста / теории может иметь другой положительный эффект, кроме краткости: вы можете начать думать о ваших тестовых данных независимо от фактического материала для тестирования.

Давайте сделаем именно это. Если вы хотите проверить метод, который принимает целочисленный аргумент, какие целые могут вызвать проблемы? Это мое предложение:

1
2
3
4
@DataPoints
  public static int[] integers() {
     return new int[]{
                     0, -1, -10, -1234567,1, 10, 1234567, Integer.MAX_VALUE, Integer.MIN_VALUE};}

Что, конечно, вызывает сбой теста в нашем примере. Если вы добавите положительное целое число в Integer.MAX_VALUE, вы получите переполнение! Итак, мы только что узнали, что наша теория в ее нынешнем виде неверна! Да, я знаю, что это очевидно, но взгляните на тесты в вашем текущем проекте. Все ли тесты, в которых используется проверка целых чисел с MIN_VALUE, MAX_VALUE, 0, положительным и отрицательным значением? Да, так и думал.

А как насчет более сложных объектов? Строки? Сроки? Коллекции? Или доменные объекты? С помощью JUnit Theories вы можете один раз настроить генераторы тестовых данных, которые будут создавать все сценарии, которые могут создавать проблемы, а затем повторно использовать их во всех ваших тестах с использованием теорий. Это сделает ваши тесты более выразительными и увеличит вероятность обнаружения ошибок.

Ссылка: Введение в теории JUnit от нашего партнера JCG Йенса Шаудера в блоге Java Advent Calendar .