Как на практике реализовать все принципы и упростить разработку тестирования BDD ?
Для чего нужны принципы SOLID
При создании программных систем использование принципов SOLID способствует созданию такой системы, которую будет легко поддерживать и расширять в течение долгого времени. Принципы SOLID — это руководства, которые также могут применяться во время работы над существующим программным обеспечением для его улучшения, например, для удаления «дурно пахнущего кода».
Примером будет интеграционное тестирование, но вам ничего не помешает реализовать для End2End
SRP принцип единой ответственности
Возьмем четыре базовые функции работы с БД
Create
Read
Update
Delete
Создадим интерфейс, класс которого будет имплементировать его
public interface CrudEntity {
Response getId(String... values);
void create(Object o,String... values);
void update(Object o,String... values);
void delete(String ...values);
}
@Service
public class User implements CrudEntity, ImageEntity, FindAll, FindMe {
public void create(Object o,String... value) {
given().when()
.body(o)
.post("employee/create");
}
}
На примере метода create передаем объект, который будет собираться из POJO.
String ... value это массив строк, через него передаем статусы кода или аргументы, которые нам будут нужны. Задать массив можно только один раз.
Open - closed открыт для расширения, закрыт для модификаций.
Создадим класс Condition в нем метод проверки, который будет возвращать Response
public ValidatableResponse bodyCheckSize(Response response, String valueOne, String valueTwo) {
return response
.then()
.body(valueOne, is(Integer.parseInt(valueTwo)));
}
Вы можете его расширить как считаете нужным.
Шаг описываем максимально просто.
@Step("получаем {string} в {string} и статус {string} в {string} и тело проверки {string}")
@Затем("получаем {string} в {string} и статус {string} в {string} и тело проверки {string}")
public void bodyPostCheck(String endpoint, String count, String statusCode, String countBody, String bodyCheck) {
Response response = apiMap.getFindAll(endpoint)
.findAll(count, statusCode);
bodyCondition.bodyCheckSize(response, bodyCheck, countBody);
}
Пример с библиотекой Hamsters, более ярко раскрывает этот принцип.
@Step("Перейти на {string} и Получить себя и проверяем {string} {string}")
@Затем("Перейти на {string} и Получить себя и проверяем {string} {string}")
public void goToEndpointAndGetMyUserAndCheck(String endpoint, String expected, String actual) {
CreateUser createUser = apiMap.getFindMe(endpoint)
.findMe()
.as(CreateUser.class);
assertThat(createUser,
hasProperty(expected, equalToIgnoringCase(actual)));
}
ApiMap класс ,который через обычный switch возвращает нужный endpoint описанный ранее в шагах Cucumber.
Описание шага Перейти на -> endpoint -> подставить параметр запроса
bodyCheckSize делаем проверку запроса через аргументы, которые мы передаем
LSP принцип подстановки Барбары Лисков
простыми словами можно сказать, что поведение наследующих классов не должно противоречить поведению, заданному базовым классом, то есть поведение наследующих классов должно быть ожидаемым для кода, использующего переменную базового типа.
https://ru.wikipedia.org/wiki/Принцип_подстановки_Барбары_Лисков
Наследуемся от абстрактного класса DataEntity, который будет хранить в себе переменные базового типа.
Подключаем в pom.xml
<dependency>
<groupId>com.github.javafaker</groupId>
<artifactId>javafaker</artifactId>
<version>1.0.2</version>
</dependency>
Faker faker = new Faker();
FakeValuesService fakeValuesService = new FakeValuesService(new Locale("en-GB"),
new RandomService());
public String generate() {
return UUID.randomUUID().toString();
}
public String firstName() {
return faker.name().firstName();
}
public String mail() {
return fakeValuesService.bothify("????##@gmail.com");
}
public String skype() {
return fakeValuesService.bothify("????##");
}
Библиотека очень гибкая и позволяет генерацию всего, что придет в голову.
https://github.com/DiUS/java-faker
ISP разделение интерфейсов
Есть класс User и Product
Пользователь воспроизводит все функции CRUD и может загружать себе картинки, а класс Product воспроизводит только CRUD.
public interface ImageEntity {
void uploadBinaryFile(String ... value);
void deleteBinaryFile(String o);
}
public class Product implements CrudEntity, FindAll
public class User implements CrudEntity, ImageEntity, FindAll, FindMe
Dependency inversion зависимость на абстракциях
Простыми словами мы НЕ должны каждый раз создавать объект, только задаем пример объекта с аргументами, а он собирается сам.
@Data
@Accessors(fluent = true)
@Component
@ToString
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CreateUser {
@JsonProperty("fullName")
private String fullName;
@JsonProperty("telegram")
private String telegram;
@JsonProperty("login")
private String login;
@JsonProperty("skype")
private String skype;
@JsonProperty("employeeProducts")
private List<Object> employeeProducts;
@JsonProperty("phone")
private String phone;
@JsonProperty("id")
private String id;
@JsonProperty("email")
private String email;
}
Если мы не будем прописывать поле, оно просто не создастся@JsonInclude(JsonInclude.Include.NON_NULL)
Дает возможность создавать объект текучем стиле @Accessors(fluent = true)
пример
new CreateUser().id("1")
.city("2")
.phone("3")
.email("4");
@Step("перейти на {string} обновить и статус код {string}")
@Затем("перейти на {string} обновить и статус код {string}")
public void updateUser(String endpoint, String code, Map<String, String> map) {
EmployerProducts employerProducts = new EmployerProducts();
CreateUser createUser = new CreateUser(
map.get("1")
, map.get("2")
, map.get("3")
, map.get("4")
, map.get("5")
, map.get("6")
, map.get("7")
, map.get("8")
, map.get("9")
, map.get("10")
, map.get("11")
, Collections.singletonList(
employerProducts.roleId(map.get("12"))
.productId(map.get("13")))
, map.get("14")
, map.get("15") != null ? map.get("15") : UUID.randomUUID().toString()
, map.get("16"), map.get("17"));
apiMap.getSelectedCrud(endpoint).update(createUser, code);
}
Тут мы через Cucumber передаем в Map аргументы String (vlue=key)
value поле которое описываем в сценарии
key кладется аргумент поля, которое мы указываем
@employeeContact
Структура сценария: API -> Employee обновление пользователя телефон,почта,телеграм
Затем перейти на "пользователь" обновить и статус код "400"
| workStartDate | 2022-02-25 |
| city | Moskov |
| workTimesheet | remote |
| phone | <phone> |
| email | <email> |
| skype | <skype> |
| telegram | <telegram> |
| id | ключ |
Примеры:
| phone | email | skype | telegram |
| +79199999992 | testovich@test.ru | skype | @testtelegram|
| +79169999902 | @test.ru_testovich | skype | @testtelegram|
Получается, что мы не зависим от конкретного объекта, сейчас он полностью абстрактен. Теперь мы можем написать сценарий на основе кейса, который написан по техникам тест дизайна и объект создастся не зависимо какие поля мы в него будем передавать. Если вы столкнетесь с тем, что у вас будут поля, которые передаются как массив, советую использовать stream
ArrayList<Integer> weekDays = (ArrayList<Integer>) map.entrySet()
.stream()
.filter(entry -> entry.getKey().contains("Пример поля"))
.filter(p -> p.getValue().length() > 0)
.map(Map.Entry::getValue)
.map(Integer::parseInt)
.collect(Collectors.toList());
Вам будет достаточно указать
Пример поля 1 пример1
Пример поля 2
Пример поля 3 пример3
Будет создан Пример поля [пример1, пример3]
Надеюсь информация будет для вас полезна и вы будете придерживаться SOLID, чтобы ваша разработка стала
Легко поддерживаемой
Гибкой
Расширяемой