Тестирование метода валидации объекта, который использует паттерн строителя исключительно без каких-либо сеттеров.

Вопрос или проблема

У меня есть агрегат 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 в каждом из тестовых случаев. Поэтому мне нужны альтернативы, если они есть.

  1. В @BeforeEach создайте строительную функцию и заполните все поля. Затем сохраните ее.

  2. В каждом тесте установите поле в 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.

Оцените материал
Добавить комментарий

Капча загружается...