Статьи

Шаблон Builder: хорошо для кода, отлично подходит для тестов

Я нашел шаблон проектирования конструктора, иногда полезный в коде, но часто полезный в тестах. Эта статья представляет собой краткое описание шаблона в целом, а затем рассмотрим рабочий пример его использования в тестах. Смотрите код в github .

Фон для шаблона Builder

Согласно книге GoF , шаблон проектирования строителя используется, чтобы «отделить построение сложного объекта от его представления, чтобы один и тот же процесс построения мог создавать разные представления». Как и большая часть книги GoF, это точное, но скучное описание.

Джош Блох в своей книге « Эффективное Java» предлагает более интересное использование для сборщиков. Проблема, которую пытался решить его подход, заключается в том, что у класса есть «больше, чем несколько» параметров, которые обычно устанавливаются через конструктор, и многие из них могут быть необязательными. Типичные решения

  • Шаблон телескопического конструктора, в котором вы предоставляете конструктору только требуемые параметры, а также дополнительные конструкторы с вариациями необязательных параметров, достигающих высшей точки в конструкторе со всеми необязательными параметрами.
    Это работает, но делает довольно грязное решение, которое имеет потенциально большое количество конструкторов для покрытия всех перестановок
  • Более простой конструктор (например, только для обязательных параметров), поддерживаемый методами установки для необязательных параметров (подход JavaBeans). Тем не менее, это может привести к тому, что объект будет находиться в несогласованном состоянии во время его конструирования и, конечно, исключит неизменность, поскольку поля не могут быть final .
  • Используйте Строителя. Это подход, рекомендованный Блохом. Клиент создает компоновщик (часто с использованием конструктора без параметров), затем вызывает сеттер-подобные методы для интересующих значений (остальные принимают значения по умолчанию), прежде чем, наконец, вызвать метод компоновки.

Несколько лет назад я присутствовал на лекции, в которой Тед Янг говорил о том, как сделать шаблон строителя на шаг дальше, используя его для построения тестовых объектов, и именно этот подход обсуждается ниже. [Обновление: см. Ответ Теда на этот пост здесь ]

Использование шаблона Builder для создания тестовых приборов

Использование Builder позволяет создавать тестовые таблицы проще и с более четкими намерениями.

Типами тестовых объектов, для которых я обычно использую этот подход Builder, являются объекты модели домена, такие как Account, User, Widget или любой другой. Я сторонник создания таких объектов неизменными .
Например:

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
public final class Account {
    private final Integer id;
    private final String name;
    private final AccountType type;
    private final BigDecimal balance;
    private final DateTime openDate;
    private final Status status;
 
    public Account(Integer id, String name, AccountType type,
                   BigDecimal balance, DateTime openDate, Status status) {
        this.id = id;
        this.name = name;
        this.type = type;
        this.balance = balance;
        this.openDate = openDate;
        this.status = status;
    }
 
    public Integer getId() {
        return id;
    }   
 
    //other getters, toString(), equals() and hashCode() omitted for brevity
 
    //no setters
}

С таким классом вы часто сталкиваетесь с проблемой, обсуждаемой Блохом. В этом примере у нас есть один конструктор, который заставляет вас устанавливать все значения, но у нас также может быть много вариантов, где некоторые значения могут быть опущены для использования значений по умолчанию. Таким образом, создание экземпляра такого класса для тестов может быть несколько болезненным, и тем более, если в нем еще больше полей, чем в этом простом примере. Вы вынуждены предоставлять значения даже для полей, которые могут вас не волновать для теста. Это также затрудняет определение того, какие значения на самом деле представляют интерес для теста, а какие — просто для компиляции.

Строитель может помочь.

пример

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
public class AccountBuilder {
 
    //account fields with default values
    Integer id = 1;
    String name = "default account name";
    AccountType type = AccountType.CHECKING;
    BigDecimal balance = new BigDecimal(0);
    DateTime openDate = new DateTime(2013, 01, 01, 0, 0, 0);
    Status status = Status.ACTIVE;
 
    public AccountBuilder() {}
 
    public AccountBuilder withId(Integer id) {
        this.id = id;
        return this;
    }
 
    public AccountBuilder withName(String name) {
        this.name = name;
        return this;
    }
 
    public AccountBuilder withType(AccountType type) {
        this.type = type;
        return this;
    }
 
    public AccountBuilder withBalance(BigDecimal balance) {
        this.balance = balance;
        return this;
    }
 
    public AccountBuilder withOpenDate(DateTime openDate) {
        this.openDate = openDate;
        return this;
    }
 
    public AccountBuilder withStatus(Status status) {
        this.status = status;
        return this;
    }
 
    public Account build() {
        return new Account(id, name, type, balance, openDate, status);
    }
}

Теперь вы можете гораздо проще создать объект Account для тестирования.

Замечания по использованию Builder для тестов

    • Значения по умолчанию

Значения по умолчанию, используемые в компоновщике, удобны, чтобы избежать исключений. Если вашему тесту нужны определенные значения для теста, лучше установить их явно, а не полагаться на какие-либо значения по умолчанию. Это делает цель вашего теста более понятной, а также сводит к минимуму риск непреднамеренного нарушения тестов, если вам когда-либо понадобится изменить значения по умолчанию (например, из-за меняющихся требований бизнеса).

    • Не финальные поля

В то время как сам класс модели предметной области является неизменным и, следовательно, имеет конечные поля. Все поля в построителе, по замыслу, не являются окончательными. Следовательно, строители не являются потокобезопасными. Так что не используйте повторно Строителей; вместо этого создайте новый экземпляр для каждого теста.

    • Порядок методов не должен быть значительным

По большей части порядок вызова методов в компоновщике не должен быть значительным, и объект не будет создан до тех пор, пока не будет вызван build (). Это делает конструктор более простым в использовании и позволяет избежать неожиданных сюрпризов.
Есть очевидные и приемлемые исключения из этого правила.
Например, позвонив

1
2
3
4
Account account = new AccountBuilder()
            .withType(AccountType.SAVING)
            .withType(AccountType.CHECKING)
            .build();

глупо, но разрешено Это просто оставило бы вас с проверкой типа.
Это хорошо, но старайтесь избегать более тонких причин путаницы, например, если у вас есть коллекция, в которую можно добавить что-то, или заменить всю коллекцию (следовательно, удаляя предыдущие добавления).

Преимущества использования Builder для тестов

    • Легко читать

Следующая декларация не особенно ясна:

1
Account account = new Account(1, "test", 10, ...);

Эта декларация намного понятнее:

1
2
3
4
5
Account account = new AccountBuilder()
                .withId(1)
                .withName("test")
                .withBalance(10)
                .build();

По словам Блоха, «шаблон Builder имитирует именованные необязательные параметры».

    • Укажите только те значения, которые действительно имеют отношение к вашему тесту.

Если ваш тест касается только баланса и статуса аккаунта:

1
2
3
4
Account account = new AccountBuilder()
                .withBalance(new BigDecimal(-100))
                .withStatus(Status.OVERDRAWN)
                .build();

В отличие от необходимости указывать каждое значение в конструкторе учетных записей.

    • Возможность создавать недопустимые объекты

Конструктор класса модели предметной области скорее всего (надеюсь!) Заставит вас создавать допустимые объекты. В ваших тестах вы можете намеренно создавать недопустимые объекты для тестирования.

Дальнейшие улучшения

Удобные методы

Вы можете добавить удобные методы для распространенных сценариев, используемых в тестировании.
Например

1
2
3
4
public AccountBuilder withNegativeBalance() {
        this.balance = new BigDecimal(-100);
        return this;
    }

Класс светильников

В дополнение к использованию класса Builder, я также нашел полезным иметь связанный класс фикстур, который предоставляет предварительно созданные экземпляры для тестов. Они могут использовать объект Builder для построения (хотя ничто не мешает вам использовать необработанные конструкторы).
Например

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class AccountFixtures {
 
    //a shortcut to creating a basic Account object
    public final Account ACCOUNT = new AccountBuilder().build();
 
    public final Account OVERDRAWN_CHECKING_ACCOUNT = new AccountBuilder()
            .withType(AccountType.CHECKING)
            .withNegativeBalance()
            .build();
 
    public final Account CLOSED_SAVING_ACCOUNT = new AccountBuilder()
            .withType(AccountType.SAVING)
            .withZeroBalance()
            .withStatus(Status.CLOSED)
            .build();
}