Создание объектов в тестах, как правило, является кропотливой работой и, как правило, создает много повторяемого и трудно читаемого кода. Существует два распространенных решения для работы со сложными тестовыми данными: Object Mother
и Test Data Builder
. Оба имеют свои преимущества и недостатки, но (умно) в сочетании могут принести новое качество ваших испытаний.
Примечание: вы уже можете найти много статей как об Object Mother
и о Object Mother
Test Data Builder
поэтому я сделаю свое описание очень кратким.
Объект Мать
Вкратце, Object Mother — это набор фабричных методов, которые позволяют нам создавать похожие объекты в тестах:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
// Object Mother public class TestUsers { public static User aRegularUser() { return new User( "John Smith" , "jsmith" , "42xcc" , "ROLE_USER" ); } // other factory methods } // arrange User user = TestUsers.aRegularUser(); User adminUser = TestUsers.anAdmin(); |
Каждый раз, когда требуется пользователь с немного разными вариациями данных, создается новый фабричный метод, который позволяет Object Mother
расти со временем. Это один из недостатков Object Mother
. Эта проблема может быть решена путем введения Test Data Builder .
Тестовый построитель данных
Test Data Builder
использует шаблон Builder
для создания объектов в модульных тестах. Краткое напоминание Builder
:
Шаблон Builder — это шаблон проектирования программного обеспечения для создания объектов. […] Задача шаблона построения состоит в том, чтобы найти решение для анти-шаблона телескопического конструктора.
Давайте рассмотрим пример Test Data Builder
:
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
public class UserBuilder { public static final String DEFAULT_NAME = "John Smith" ; public static final String DEFAULT_ROLE = "ROLE_USER" ; public static final String DEFAULT_PASSWORD = "42" ; private String username; private String password = DEFAULT_PASSWORD; private String role = DEFAULT_ROLE; private String name = DEFAULT_NAME; private UserBuilder() { } public static UserBuilder aUser() { return new UserBuilder(); } public UserBuilder withName(String name) { this .name = name; return this ; } public UserBuilder withUsername(String username) { this .username = username; return this ; } public UserBuilder withPassword(String password) { this .password = password; return this ; } public UserBuilder withNoPassword() { this .password = null ; return this ; } public UserBuilder inUserRole() { this .role = "ROLE_USER" ; return this ; } public UserBuilder inAdminRole() { this .role = "ROLE_ADMIN" ; return this ; } public UserBuilder inRole(String role) { this .role = role; return this ; } public UserBuilder but() { return UserBuilder .aUser() .inRole(role) .withName(name) .withPassword(password) .withUsername(username); } public User build() { return new User(name, username, password, role); } } |
В нашем тесте мы можем использовать конструктор следующим образом:
1
2
3
4
5
6
7
|
UserBuilder userBuilder = UserBuilder.aUser() .withName( "John Smith" ) .withUsername( "jsmith" ); User user = userBuilder.build(); User admin = userBuilder.but() .withNoPassword().inAdminRole(); |
Приведенный выше код выглядит довольно мило. У нас есть свободный API, который улучшает читаемость тестового кода и наверняка устраняет проблему наличия нескольких фабричных методов для вариаций объектов, которые нам нужны в тестах при использовании Object Mother
.
Обратите внимание, что я добавил некоторые значения по умолчанию для свойств, которые могут не иметь отношения к большинству тестов. Но поскольку они определены как публичные константы, их можно использовать в утверждениях, если мы этого хотим.
Примечание: пример, используемый в этой статье, является относительно простым. Он используется для визуализации решения.
Мать объекта и построитель тестовых данных объединены
Ни одно из решений не является идеальным. Но что, если мы объединим их? Представьте, что Object Mother
возвращает Test Data Builder
. Имея это, вы можете манипулировать состоянием компоновщика перед вызовом терминальной операции. Это своего рода шаблон.
Посмотрите на пример ниже:
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
|
public final class TestUsers { public static UserBuilder aDefaultUser() { return UserBuilder.aUser() .inUserRole() .withName( "John Smith" ) .withUsername( "jsmith" ); } public static UserBuilder aUserWithNoPassword() { return UserBuilder.aUser() .inUserRole() .withName( "John Smith" ) .withUsername( "jsmith" ) .withNoPassword(); } public static UserBuilder anAdmin() { return UserBuilder.aUser() .inAdminRole() .withName( "Chris Choke" ) .withUsername( "cchoke" ) .withPassword( "66abc" ); } } |
Теперь TestUsers
предоставляет фабричный метод для создания похожих тестовых данных в наших тестах. Он возвращает экземпляр компоновщика, поэтому мы можем быстро и удобно изменять объект в нашем тесте по мере необходимости:
1
2
|
UserBuilder user = TestUsers.aUser(); User admin = user.but().withNoPassword().build(); |
Преимущества велики. У нас есть шаблон для создания похожих объектов, и у нас есть возможности построителя, если нам нужно изменить состояние возвращаемого объекта перед его использованием.
Обогащение тестового построителя данных
Размышляя о вышесказанном, я не уверен, действительно ли необходимо хранить отдельную Object Mother
. Мы могли бы легко переместить методы из Object Mother
напрямую в Test Data Builder
:
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
|
public class UserBuilder { public static final String DEFAULT_NAME = "John Smith" ; public static final String DEFAULT_ROLE = "ROLE_USER" ; public static final String DEFAULT_PASSWORD = "42" ; // field declarations omitted for readability private UserBuilder() {} public static UserBuilder aUser() { return new UserBuilder(); } public static UserBuilder aDefaultUser() { return UserBuilder.aUser() .withUsername( "jsmith" ); } public static UserBuilder aUserWithNoPassword() { return UserBuilder.aDefaultUser() .withNoPassword(); } public static UserBuilder anAdmin() { return UserBuilder.aUser() .inAdminRole(); } // remaining methods omitted for readability } |
Благодаря этому мы можем поддерживать создание User
данных внутри одного класса.
Обратите внимание, что в этом Test Data Builder
является тестовым кодом. В случае, если у нас уже есть конструктор в производственном коде, создание Object Mother
возвращающего экземпляр Builder
звучит как лучшее решение.
Как насчет изменяемых объектов?
С подходом Test Data Builder
есть некоторые возможные недостатки, когда речь идет о изменяемых объектах. И во многих приложениях я в основном имею дело с изменчивыми объектами (иначе говоря, beans
или anemic data model
), и, вероятно, многие из вас также.
Шаблон Builder
предназначен для создания неизменных ценностных объектов — в теории. Как правило, если мы имеем дело с изменчивыми объектами, на первый взгляд, построитель Test Data Builder
может показаться дублированием:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// Mutable class with setters and getters class User { private String name; public String getName() { ... } public String setName(String name) { ... } // ... } public class UserBuilder { private User user = new User(); public UserBuilder withName(String name) { user.setName(name); return this ; } // other methods public User build() { return user; } } |
В тесте мы можем создать такого пользователя:
1
2
3
4
|
User aUser = UserBuiler.aUser() .withName( "John" ) .withPassword( "42abc" ) .build(); |
Вместо:
1
2
3
|
User aUser = new User(); aUser.setName( "John" ); aUser.setPassword( "42abc" ); |
В таком случае создание Test Data Builder
является компромиссом . Это требует написания большего количества кода, который необходимо поддерживать. С другой стороны, читаемость значительно улучшена.
Резюме
Управление тестовыми данными в модульных тестах не легкая работа. Если вы не найдете хорошего решения, у вас будет много стандартного кода, который трудно читать и понимать, и который трудно поддерживать. С другой стороны, для этой проблемы нет решения «серебряной пули». Я экспериментировал со многими подходами. В зависимости от масштаба проблемы, с которой мне нужно иметь дело, я выбираю другой подход, иногда комбинируя несколько подходов в одном проекте.
Как вы справляетесь с построением данных в ваших тестах?
Ресурсы
- Петри Кайнулайнен: написание чистых тестов — новый считается вредным
-
Growing Object-Oriented Software, Guided by Tests
— Глава 22:Constructing Complex Test Data
.
Ссылка: | Создатели тестовых данных и объектная мать: еще один взгляд нашего партнера по JCG Рафаля Боровца на блог Codeleak.pl . |