Создание объектов в тестах, как правило, является кропотливой работой и, как правило, создает много повторяемого и трудно читаемого кода. Существует два распространенных решения для работы со сложными тестовыми данными: 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 Motherpublic class TestUsers { public static User aRegularUser() { return new User("John Smith", "jsmith", "42xcc", "ROLE_USER"); } // other factory methods}// arrangeUser 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 gettersclass 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 . |