Зачем Link to heading
Действительно, зачем? Зачем делать серию статей о TDD?
- 📢 На конференциях об этом говорят.
- 🎓 На собеседовании пояснить за TDD могут.
- 💪 Кто-то даже пробовал.
- ❤️ Кому-то даже понравилось.
А кто пробовал использовать TDD на большом закостенелом проекте? Где размер объектов подогревает стулья, а после прочтения ТЗ хочется выйти в окно. Ну и как? Работает тест дривен магия? Скорее всего нет. 💩
Расскажу как не вываливаться из цикла red-green-refactor на том самом большом проекте.
Что мешает Link to heading
Для комфортного кодирования в потоке red-green-refactor тесты должны быть небольшого размера, методы простыми для восприятия.
На словах просто, а как насчет кода?
Исходные данные Link to heading
Допустим, доменная модель состоит из Person
и Organisation
. На деле в разы больше, но для примера достаточно.
public abstract class AbstractCustomer {
private Address address;
public abstract String getWelcomeName();
public String getCountry() {
return address.getCountry();
}
// geters and setters
}
public class Organisation extends AbstractCustomer {
private String organisationName;
@Override
public String getWelcomeName() {
return organisationName;
}
// geters and setters
}
public class Person extends AbstractCustomer {
private String firstName;
private String lastName;
@Override
public String getWelcomeName() {
return firstName + " " + lastName;
}
// geters and setters
}
public class Address {
private String street;
private String city;
private String country;
private int house;
private int index;
// getters and setters
}
Для создания объектов используется паттерн JavaBeans. Это когда конструктор пустой, а объект настраивается через сеттеры. Да, не лучший выбор, но и проект с историческим наследием. Вот статья о разных видах создания объектов в java. Спойлер: используйте билдер.
Мы будем работать с тем, что есть. 💁♀️
Задача Link to heading
Разработать сервис Hostel. Hostel умеет приветствовать клиентов фразой "Welcome in hoselName, customer.getWelcomeName(). customer.getCountry() is great!"
. Вот и вся задача.
Погнали!
Первый заход Link to heading
Определяем интерфейс Hostel
public interface Hostel {
String getWelcome(AbstractCustomer customer);
}
И дефолтную имплементацию
public class HostelImpl implements Hostel {
private String hostelName;
public HostelImpl(String hostelName) {
this.hostelName = hostelName;
}
@Override
public String getWelcome(AbstractCustomer customer) {
return null;
}
Пишем тест
class HostelImplTestWithoutSmartBuilder {
Hostel service;
@BeforeEach
void setUp() {
service = new HostelImpl("Best hostel");
}
@Test
void shouldWelcomePerson() {
// Given
Address address = new Address();
address.setCountry("Russia");
address.setCity("Moscow");
// And
Person person = new Person();
person.setFirstName("Alexander");
person.setLastName("Pakhomov");
person.setAddress(address);
// When
String welcomeMessage = service.getWelcome(person);
// Then
assertThat(welcomeMessage).isEqualTo("Welcome in Best hostel, Alexander Pakhomov. Russia is great!");
}
Предлагаю остановиться и посмотреть на тест.
Для такого простого кейса слишком много кода. Проблема с секцией //Given
.
|
|
Слишком громоздко. Суть теста замыливается созданием объектов Person
и Address
. Нужно это упростить.
Второй заход Link to heading
Выносим создание объектов в отдельный метод Link to heading
Мысль очевидная и в большинстве случаев работaет. Но когда дело доходит до огромного количества полей у объектов, то получаем что-то вроде
Person givenPerson = createSamplePerson("Saha", "Pushkin", "Russia","Kolotushkina 29", null, null, 0, "")
и это еще лайт. Напомню, проект с историческим наследием!
Да, код можно улучшить: вынести параметры в переменные, добавить еще методов. Но это не решение проблемы, это подавление симптомов. Нужно использовать другой механизм создания объектов.
Третий заход Link to heading
Используем Builder Link to heading
// Given
var person = Person.builder()
.firstName("Alexander")
.lastName("Pakhomov")
.address(Address.builder()
.country("Russia")
.city("Moscow")
.build())
.build()
);
Уже лучше. Параметры именованны и типизированны. Но что тут не так?
Мы хотим писать в TDD парадигме. Тесты должны быть простыми, отражать только спецификацию. Мы должны читать тесты как описание поведения объекта. Наличие Person.builder()
и никому (в тесте) не нужных вызовов .build()
усложняет чтение кода.
|
|
Решение – SmartBuilder 👷♂️ Link to heading
Как это выглядит в итоге Link to heading
Создание Person
// Given
var person = given(
person().firstName("Alexander")
.lastName("Pakhomov")
.address(address().country("Russia").city("Moscow"))
);
Создание Organisation
// Given
var organisation = given(
organisation()
.organisationName("Apple Inc")
.address(address().country("Russia").city("Moscow"))
);
Имплементация Link to heading
Базовый интерфейс для всех SmartBuilder-ов
public interface GenericBuilder<T, B extends GenericBuilder<T, B>> {
T build();
}
Абстрактный билдер для AbstractCustormer
public abstract class CustomerBuilder<T extends AbstractCustomer, B extends CustomerBuilder<T, B>>
implements GenericBuilder<T,B> {
private Address address;
public GenericBuilder<T,B> address(Address address) {
this.address = address;
return this;
}
public GenericBuilder<T,B> address(AddressBuilder<Address> addressBuilder) {
this.address = addressBuilder.build();
return this;
}
protected abstract T createCustomer();
@Override
public T build() {
T customer = createCustomer();
customer.setAddress(address);
return customer;
}
}
Билдер для Person
public class PersonBuilder<T extends Person> extends CustomerBuilder<T, PersonBuilder<T>> {
private String firstName;
private String lastName;
public PersonBuilder<T> firstName(String firstName) {
this.firstName = firstName;
return this;
}
public PersonBuilder<T> lastName(String lastName) {
this.lastName = lastName;
return this;
}
@Override
protected T createCustomer() {
T person = (T) new Person();
person.setFirstName(this.firstName);
person.setLastName(this.lastName);
return person;
}
}
Билдер для Organisation
public class OrganisationBuilder<T extends Organisation> extends CustomerBuilder<T, OrganisationBuilder<T>> {
private String organisationName;
public OrganisationBuilder<T> organisationName(String organisationName) {
this.organisationName = organisationName;
return this;
}
@Override
protected T createCustomer() {
T organisation = (T) new Organisation();
organisation.setOrganisationName(organisationName);
return organisation;
}
}
И финалочка - DSL в виде статичных методов
public class GenericBuilderDsl {
public static <T, B extends GenericBuilder<T, B>> T given(GenericBuilder<T, B> builder) {
return builder.build();
}
public static PersonBuilder<Person> person() {
return new PersonBuilder<>();
}
public static OrganisationBuilder<Organisation> organisation() {
return new OrganisationBuilder<>();
}
public static AddressBuilder<Address> address() {
return new AddressBuilder<>();
}
}
Вот и все! SmartBuilder-ы удобны для написания тестов, которые может прочитать даже ваш PM.
Если вам понравилась эта идея:
- вот имплементация
- тут можно посмотреть тест без SmartBuilder
- тут тест с SmartBuilder
Подписывайтесь на душный канал и твиттер. В следующей статье пойдем дальше. 🤘