Вопрос или проблема
У меня есть агрегат Employee
, который использует паттерн строительства для задания своих полей в процессе создания объекта. У него есть различные методы жизненного цикла, которые изменяют состояние, но нет прямых сеттеров. У него также есть метод validate(), который проверяет некоторые условия для полей, установленных строительной функцией, и поэтому может завершиться неудачей (выбросить исключение) по нескольким причинам.
Агрегат выглядит примерно так:
public class Employee {
private String field1;
private String field2;
//скажем 10 полей
public static Builder builder() {
return new Builder();
}
public static class Builder {
// поля и методы строительной функции
}
// публичные методы жизненного цикла
public void validate() {
if (field1 == null) {
//выбросить пользовательское исключение
}
if (field2 == null) {
//выбросить пользовательское исключение
}
// Больше проверок условий для полей (не все проверки на null)
}
}
Теперь мой класс тестов организован следующим образом:
public class EmployeeTest {
@Test
public void givenNewEmployee_IfField1IsMissing_thenThrowsException() {
//Подготовка
Employee testEmployee = Employee.builder()
.field2(nonNullValue)
//другие поля
.build();
Assertions.assertThrows(CustomException.class, testEmployee::validate);
}
@Test
public void givenNewEmployee_IfField2IsMissing_thenThrowsException() {
//Подготовка
Employee testEmployee = Employee.builder()
.field1(nonNullValue)
//другие поля
.build();
//Тест
Assertions.assertThrows(CustomException.class, testEmployee::validate);
}
}
В приведенном выше примере для каждого тестового случая мне нужно создавать весь объект теста testEmployee
, исключая одно поле в методе build()
. Это делает мои тестовые случаи громоздкими и требует много строк кода.
Мой вопрос:
Существует ли способ, который я могу использовать для настройки объекта testEmployee
один раз, а затем устанавливать отдельные поля в null в каждом из тестовых случаев, чтобы мои тесты были краткими и не требовали создания всего testEmployee
?
Я знаком с аннотациями @BeforeAll
и @BeforeEach
для настройки тестов. Но поскольку мой класс Employee
не предоставляет никаких сеттеров, у меня нет возможности настроить один валидный testEmployee
в @BeforeEach
и затем устанавливать отдельные поля в null в каждом из тестовых случаев. Поэтому мне нужны альтернативы, если они есть.
-
В
@BeforeEach
создайте строительную функцию и заполните все поля. Затем сохраните ее. -
В каждом тесте установите поле в
null
, затем создайте и проверьте.
Пример:
class EmployeeTest {
Employee.Builder builder;
@BeforeEach
void init(){
builder = Employee.builder()
.field1("some value")
.field2("another value")
}
@Test
void testField1Value() {
Employee e = builder.field1(null).build();
e.validate();
}
}
В качестве альтернативы вы можете создать метод toBuilder
. Тогда вы сможете сделать employee.toBuilder().field1(null).build().validate()
.
Ответ или решение
Для оптимизации тестирования классов, использующих шаблон проектирования Builder, а также для упрощения проверки методов валидации созданного объекта без необходимости многократного повторного построения всего объекта, вы можете рассмотреть несколько стратегий. В вашем случае с агрегатом Employee
, у которого нет прямых сеттеров, вы можете упростить тесты, проанализировав предложенные альтернативы.
Подход 1: Использование шаблона Builder
На первом этапе, в методе @BeforeEach
, вы можете создать объект Builder
, инициализировав все его поля. Затем на каждом тесте вы сможете изменять только необходимые поля, оставшиеся поля будут автоматически инициализироваться значениями, установленными в методе инициализации. Пример кода может выглядеть так:
public class EmployeeTest {
private Employee.Builder builder;
@BeforeEach
void init() {
builder = Employee.builder()
.field1("значение1")
.field2("значение2");
// Инициализация остальных полей
}
@Test
void givenNewEmployee_IfField1IsMissing_thenThrowsException() {
// Устанавливаем field1 в null и строим объект
Employee testEmployee = builder.field1(null).build();
// Проверяем выброс исключения
Assertions.assertThrows(CustomException.class, testEmployee::validate);
}
@Test
void givenNewEmployee_IfField2IsMissing_thenThrowsException() {
// Устанавливаем field2 в null и строим объект
Employee testEmployee = builder.field2(null).build();
// Проверяем выброс исключения
Assertions.assertThrows(CustomException.class, testEmployee::validate);
}
}
Подход 2: Метод toBuilder
Второй вариант заключается в добавлении метода toBuilder()
в класс Employee
. Этот метод будет возвращать новый объект Builder
, инициализируемый текущим состоянием объекта. Это даст возможность изменять нужное поле, не затрагивая остальные. Пример реализации и тестирования:
public class Employee {
private String field1;
private String field2;
// Прочие поля...
public static Builder builder() {
return new Builder();
}
public Employee toBuilder() {
return builder()
.field1(this.field1)
.field2(this.field2);
// Копирование остальных полей
}
// Вложенный класс Builder...
public void validate() {
if (field1 == null) {
throw new CustomException("field1 cannot be null");
}
if (field2 == null) {
throw new CustomException("field2 cannot be null");
}
// Прочие проверки
}
}
// Тестирование
public class EmployeeTest {
@Test
void givenNewEmployee_IfField1IsMissing_thenThrowsException() {
Employee testEmployee = Employee.builder()
.field1("значение1")
.field2("значение2")
// Инициализация остальных полей...
.build();
// Используем toBuilder() для изменения значения поля
Assertions.assertThrows(CustomException.class, () ->
testEmployee.toBuilder().field1(null).build().validate());
}
// Другие тесты...
}
Заключение
Предложенные подходы оптимизируют ваше тестирование и делают его более чистым и понятным. Используя Builder
, вы можете легко изменять только те поля, которые хотите протестировать, при этом сохраняя порядок и читаемость кода. Эти методы не только помогают избежать дублирования кода, но и обеспечивают большую гибкость и простоту в управлении процессом тестирования.
Создание хорошо организованных и поддерживаемых тестов — важный аспект разработки ПО, и указанные подходы помогут вам достичь этой цели, несмотря на ограничения самого класса Employee
.