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