🦗
Рассмотрим как может эволюционировать подход к созданию DTO для тестов на примере следующего класса:
@Data
public class User {
@NotEmpty
private String firstName;
@NotEmpty
private String secondName;
@NotNull
@Pattern(regexp = "\\d{10}")
private String phone;
@NotNull
@INN(type = INN.Type.INDIVIDUAL)
private String inn;
}
Здесь аннотация @Data
из библиотеки Lombok, а остальные аннотации из стандарта Bean Validation и ее реализации Hibernate Validator.
Предположим, мы хотим проверить, что валидация настроена корректно и пишем на это тесты.
В тестовом методе
Когда мы пишем первый тест, то обычно создаем DTO в самом тестовом методе:
@Test
void allInMethod() {
User user = new User();
user.setFirstName("Claudia");
user.setSecondName("Coca");
user.setPhone("8005553535");
user.setInn("796166717907");
boolean isValid = validator.isValid(user);
assertTrue(isValid);
}
В приватном методе тестового класса
Как только одна DTO-шка начинает использоваться в нескольких тестах, мы выносим ее создание в приватный метод класса.
@Test
void inPrivateMethod() {
User user = createValidUser();
boolean isValid = validator.isValid(user);
assertTrue(isValid);
}
@Test
void inPrivateMethodFail() {
User user = createValidUser();
user.setInn("012345678901");
boolean isValid = validator.isValid(user);
assertFalse(isValid);
}
private User createValidUser() {
final User user = new User();
user.setFirstName("Claudia");
user.setSecondName("Coca");
user.setPhone("8005553535");
user.setInn("796166717907");
return user;
}
В статическом методе утилитного класса
Через какое-то время нам требуется генерировать ту же самую DTO и для других тестовых классов, тогда мы выносим ее создание в утилитный класс:
public class TestUtils {
public static User createUser() {
final User user = new User();
user.setFirstName("Claudia");
user.setSecondName("Coca");
user.setPhone("8005553535");
user.setInn("796166717907");
return user;
}
}
Затем оказывается, что в различных тестах нужно обязательно задавать значения некоторым полям при создании DTO и тогда появляется большое количество перегруженных методов. Зачастую в таком случае дублирования кода уже не избежать:
public class TestUtils {
public static User createUser() {
return createUser("796166717907");
}
public static User createUser(String inn) {
final User user = new User();
user.setFirstName("Claudia");
user.setSecondName("Coca");
user.setPhone("8005553535");
user.setInn(inn);
return user;
}
public static User createUser(String firstName, String secondName) {
final User user = new User();
user.setFirstName(firstName);
user.setSecondName(secondName);
user.setPhone("8005553535");
user.setInn("796166717907");
return user;
}
}
В результате использования таких телескопических методов генерации утилитный класс начинает разрастаться до невероятных объемов. Жить с этим становиться решительно невозможно!
Использование сочетания Factory и Builder
И тут на помощь приходит использование двух паттернов: Factory и Builder.
Для каждого часто используемого в тестах DTO можно создавать отдельный класс-фабрику, который будет заниматься созданием его объектов. При этом возвращать он будет не готовый объект, а предзаполненный билдер:
public final class UserFactory {
private final User user;
private UserFactory() {
this.user = new User();
}
public static UserFactory validUser() {
return new UserFactory()
.firstName("Josh")
.secondName("Long")
.phone("8005553535")
.inn("796166717907");
}
public UserFactory firstName(final String firstName) {
user.setFirstName(firstName);
return this;
}
public UserFactory secondName(final String secondName) {
user.setSecondName(secondName);
return this;
}
public UserFactory phone(final String phone) {
user.setPhone(phone);
return this;
}
public UserFactory inn(final String inn) {
user.setInn(inn);
return this;
}
public User build() {
return user;
}
}
Вызов из тестового кода такой фабрики будет выглядеть достаточно симпатично:
@Test
void inFactoryFail() {
User user = UserFactory.validUser()
.inn("012345678901")
.build();
boolean isValid = validator.isValid(user);
assertFalse(isValid);
}
Количество кода можно сократить, если Builder будет реализован в самом классе DTO (например с помощью аннотации @Builder
из библиотеки Lombok):
@Data
@Builder
public class User {
/* ... */
}
Тогда фабрика тестовых объектов будет чуть более лаконичной:
public final class UserFactory {
private UserFactory() {}
public static User.UserBuilder hardcode() {
return User.builder()
.firstName("Josh")
.secondName("Long")
.phone("8005553535")
.inn("796166717907");
}
}
Еще в такой фабрике можно сделать несколько методов генерации, каждый из которых будет генерировать какой-либо валидный инвариант класса.
Представим себе класс, в котором может быть заполнен либо один набор полей, либо набор других, но не оба вместе (достался нам от предыдущего разработчика):
@Value
@Builder
public class UserFilter {
INN.Type type;
LocalDate day; //Либо заполнен day
LocalDateTime from; //Либо from + to
LocalDateTime to;
}
Тогда фабрика для него будет выглядеть следующим образом:
public final class UserFilterFactory {
private UserFilterFactory() { }
public static UserFilter.UserFilterBuilder byDay() {
return UserFilter.builder()
.type(INN.Type.INDIVIDUAL)
.day(LocalDate.of(2021, 3, 8));
}
public static UserFilter.UserFilterBuilder byInterval() {
return UserFilter.builder()
.type(INN.Type.INDIVIDUAL)
.from(LocalDateTime.of(
LocalDate.of(2021, 3, 8),
LocalTime.of(10, 0)
)
)
.to(LocalDateTime.of(
LocalDate.of(2021, 3, 8),
LocalTime.of(20, 30)
)
);
}
}