Статьи

Использование Pattern Builder в тестах JUnit

Это не предназначено, чтобы быть очень техническим постом. Цель этого поста — дать вам несколько советов, которые сделают ваш жизненный цикл тестирования JUnit более простым, позволят вам писать сложные сценарии для тестов за считанные минуты с бонусом на наличие чрезвычайно читаемых тестов.

В модульных тестах есть две основные части, которые требуют написания большого количества кода начальной загрузки:

  • часть установки: построение вашего начального состояния требует построения начальных объектов, которые будут поданы в вашу SUT (тестируемая система)
  • часть утверждения: построение желаемого изображения ваших выходных объектов и утверждение только на основе необходимых данных.

Чтобы уменьшить сложность построения объектов для тестов, я предлагаю использовать шаблон Builder в следующей интерпретации:

Вот доменный объект:

1
2
3
4
5
6
public class Employee {
    private int id;
    private String name;
    private Department department;
 
    //setters, getters, hashCode, equals, toString methods

Конструктор для этого объекта домена будет выглядеть следующим образом:

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
42
43
44
45
46
public class EmployeeBuilder {
    private Employee employee;
 
    public EmployeeBuilder() {
        employee = new Employee();
    }
 
    public static EmployeeBuilder defaultValues() {
        return new EmployeeBuilder();
    }
 
    public static EmployeeBuilder clone(Employee toClone) {
        EmployeeBuilder builder = defaultValues();
        builder.setId(toClone.getId());
        builder.setName(toClone.getName());
        builder.setDepartment(toClone.getDepartment());
        return builder;
    }
 
    public static EmployeeBuilder random() {
        EmployeeBuilder builder = defaultValues();
        builder.setId(getRandomInteger(0, 1000));
        builder.setName(getRandomString(20));
        builder.setDepartment(Department.values()[getRandomInteger(0, Department.values().length - 1)]);
        return builder;
    }
 
    public EmployeeBuilder setId(int id) {
        employee.setId(id);
        return this;
    }
 
    public EmployeeBuilder setName(String name) {
        employee.setName(name);
        return this;
    }
 
    public EmployeeBuilder setDepartment(Department dept) {
        employee.setDepartment(dept);
        return this;
    }
 
    public Employee build() {
        return employee;
    }
}

Как видите, у нас есть несколько заводских методов:

1
2
3
public static EmployeeBuilder defaultValues()
    public static EmployeeBuilder clone(Employee toClone)
    public static EmployeeBuilder random()

Эти методы возвращают разных компоновщиков:

  • defaultValues: некоторые жестко закодированные значения для каждого поля (или значения по умолчанию Java — текущая реализация)
  • clone: ​​примет все значения из исходного объекта и даст вам возможность изменить только некоторые
  • random: генерирует случайные значения для каждого поля. Это очень полезно, когда у вас есть много полей, которые вам не нужны в тесте, но вам нужно, чтобы они были инициализированы. Методы getRandom * определены статически в другом классе.

Вы можете добавить другие методы, которые будут инициализировать ваш строитель в соответствии с вашими потребностями.

Также строитель может справиться со сборкой некоторых объектов, которые не так легко построить и изменить. Например, давайте немного изменим объект Employee и сделаем его неизменным:

1
2
3
4
5
6
public class Employee {
    private final int id;
    private final String name;
    private final Department department;
    ...
}

Теперь мы потеряли возможность менять поля по своему желанию. Но используя конструктор в следующей форме, мы можем восстановить эту возможность при построении объекта:

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
42
43
44
45
46
47
public class ImmutableEmployeeBuilder {
    private int id;
    private String name;
    private Department department;
 
    public ImmutableEmployeeBuilder() {
    }
 
    public static ImmutableEmployeeBuilder defaultValues() {
        return new ImmutableEmployeeBuilder();
    }
 
    public static ImmutableEmployeeBuilder clone(Employee toClone) {
        ImmutableEmployeeBuilder builder = defaultValues();
        builder.setId(toClone.getId());
        builder.setName(toClone.getName());
        builder.setDepartment(toClone.getDepartment());
        return builder;
    }
 
    public static ImmutableEmployeeBuilder random() {
        ImmutableEmployeeBuilder builder = defaultValues();
        builder.setId(getRandomInteger(0, 1000));
        builder.setName(getRandomString(20));
        builder.setDepartment(Department.values()[getRandomInteger(0, Department.values().length - 1)]);
        return builder;
    }
 
    public ImmutableEmployeeBuilder setId(int id) {
        this.id = id;
        return this;
    }
 
    public ImmutableEmployeeBuilder setName(String name) {
        this.name = name;
        return this;
    }
 
    public ImmutableEmployeeBuilder setDepartment(Department dept) {
        this.department = dept;
        return this;
    }
 
    public ImmutableEmployee build() {
        return new ImmutableEmployee(id, name, department);
    }
}

Это очень полезно, когда нам сложно конструировать объекты или нам нужно изменить поля, которые являются окончательными.

И вот его окончательный результат:

Без строителей:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
@Test
    public void changeRoleTestWithoutBuilders() {
        // building the initial state
        Employee employee = new Employee();
        employee.setId(1);
        employee.setDepartment(Department.DEVELOPEMENT);
        employee.setName("John Johnny");
 
        // testing the SUT
        EmployeeManager employeeManager = new EmployeeManager();
        employeeManager.changeRole(employee, Department.MANAGEMENT);
 
        // building the expectations
        Employee expectedEmployee = new Employee();
        expectedEmployee.setId(employee.getId());
        expectedEmployee.setDepartment(Department.MANAGEMENT);
        expectedEmployee.setName(employee.getName());
 
        // assertions
        assertThat(employee, is(expectedEmployee));
    }

Со строителями:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Test
    public void changeRoleTestWithBuilders() {
        // building the initial state
        Employee employee = EmployeeBuilder.defaultValues().setId(1).setName("John Johnny").setDepartment(Department.DEVELOPEMENT).build();
 
        // building the expectations
        Employee expectedEmployee = EmployeeBuilder.clone(employee).setDepartment(Department.MANAGEMENT).build();
 
        // testing the SUT
        EmployeeManager employeeManager = new EmployeeManager();
        employeeManager.changeRole(employee, Department.MANAGEMENT);
 
        // assertions
        assertThat(employee, is(expectedEmployee));
    }

Как видите, размер теста намного меньше, а построение объектов стало намного проще (и лучше, если у вас есть лучший формат кода). Разница больше, если у вас есть более сложный объект домена (что более вероятно в реальных приложениях и особенно в устаревшем коде).

Повеселись!

Ссылка: Использование шаблона Builder в тестах JUnit от нашего партнера по JCG Стефана Булзана в блоге Java Advent Calendar .