🦗

Рассмотрим как может эволюционировать подход к созданию 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)
                )
            );
    }
}

Ссылки