Как стать автором
Обновить

Простой, полезный проект интеграционных тестов

Уровень сложностиСредний
Время на прочтение21 мин
Количество просмотров5.1K

Про что будет идти речь

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

По отзывам моих ревьюеров, эта статья -"Инструкция по входу в автоматизированное тестирование и настройка фрейма".

В статье приведена информация о специфике, мотивации создания интеграционных тестов, обосновании выбранных технологий. Как бонус, в конце будет ссылка на обезличенный проект, который с минимальной адаптацией, запускается в работу в краткие сроки. Но "подпилить" его придется.

Используемые технологии

Технологический стек, используемый для создания и запуска интеграционных тестов, следующий:

  • Java 17;

  • Rest Assured;

  • Maven;

  • Allure;

  • GitLab;

Дальше поговорим о роли и месте каждой технологии в созданных интеграционных тестах.

Какую пользу может принести эта статья

Для тех, кто, не является профессиональным автоматизатором тестирования, но осознает важность интеграционных тестов, эта статья и приложенные ссылки помогут двинуться дальше. Вам понадобиться помощь с настройкой инфраструктуры и подготовкой скриптов devOps для исполнения тестов. Это типовые действия, с которыми могут помочь devOps специалисты. Если такой помощи нет, то придется погрузиться в дополнительные аспекты настройки инфраструктуры. В этих аспектах эта статья не поможет.

Введение

Надеюсь, что к этому моменту решение о том, стоит ли читать статью дальше - принято. Постараюсь не грузить читателя специфической информацией, но какие-то аспекты контекста расскажу, чтобы сформировать полноценное понимание ситуации, в которой оказался сам, и который может быть вам близок. Я Занимаюсь развитием системы - ILOG ML (далее - IML).

Что за система?

IML представляет собой слабосвязанную сервисно-микросервисную систему, в которой существуют 3 основных слоя:

  • Слой 1 →  Слой проверок (около 20)

    • Сколько сервисов

      • 30;

    • Что это

      • Постоянно развивающийся и пополняющийся набор микросервисов;

      • Каждый микросервис реализует взаимодействие с конкретным источником данных;

    • Какую функцию выполняют

      • Реализует минимально необходимую валидацию данных;

      • Формат запроса/ответа сервисов единообразно типизирован;

  • Слой 2 → Универсальный адаптер проверок

    • Сколько сервисов

      • 1;

    • Что это

      • Получает данные из слоя проверок;

      • Обрабатывает и передает данные в продуктовые сервисы;

    • Какую функцию выполняют

      • Представляет загружаемые данные в типизированном формате;

      • Выполняет обработку недоступности источников

  • Слой 3 → Продуктовые сервисы

    • Сколько сервисов

      • 10;

    • Что это

      • Микросервисы по бизнес направленности;

      • Каждый страховой продукт реализует одну или несколько проверок из универсального адаптера и еще что-то по бизнес логике;

    • Какую функцию выполняют

      • Агрегация проверок по бизнес направленности;

IML не имеет хранилища данных. Эта система состоит из связанных интеграционных слоев (Рис.1).

Рис.1
Рис.1

Что с тестированием?

Команда, занимающаяся IML состоит из 2 человек. Архитектор, который кроме IML занимается еще и ILOG и ваш покорный слуга. Еще, эпизодически, для развития привлекаются devOps специалисты-молодцы. Тестирование, в большей части, лежит на мне. IML не полностью покрыт юнит тестами, но они есть. По договоренности, Quality gate определен - 85%. Что не соответствует этому показателю, целенаправленно поднимается до установленного порога, новое сразу делается с таким порогом. Функциональное тестирование провожу сам, исправляю и переделываю все, что работает не так, как планировалось. Ядро IML - Универсальный адаптер проверок. Как следует из названия, это сервис, имплементирует проверки первого слоя, обработку и подготовку данных для 3-го слоя. С точки зрения проектирования - это структурный шаблон адаптер. Используются объекты 1-го слоя, через общие интерфейсы, абстрактный класс, классы наследники, для подготовки данных в унифицированном виде. Это сердце IML и критичная сетевая точка, для которой нужно мониторить доступность IML и доступность клиентов - микросервисов 1 и 3 слоя. Наши выводы - интеграционное тестирование требуется в первую очередь для этого слоя.

Стимулы изменений

Стимулов к внедрению интеграционного тестирования было три:

  • Первый - инструмент экспресс-проверки готовности сделанных изменений. Хотелось локально, после готовности merge request, понимать, насколько изменения готовы к внедрению в тестовое окружение;

  • Второй - хотелось понимать готовность конкретной среды - dev/test/prod. Изменения могут работать, но какой-то из сервисов будет не доступен. В таком случае передавать сделанное дальше, по конвейеру разработки преждевременно. Нужно починить или попросить, чтобы починили не работающее или работающее не так и только после этого передавать сделанное дальше по pipeline разработки;

  • Третий - после deploy в продуктивный IML хотелось быть уверенным в том, что сервис работает. Иногда он может не работать сам, иногда могут не работать сервисы первого слоя или источники из которых поставляются данные. Если такое произошло хотелось бы первым узнать об этом и сразу чинить или просить чтобы починили, не дожидаясь работы службы поддержки;

    В чем дискомфорт?

    Я один занимаюсь сопровождением и развитием IML. Если что-то сломалось и не работает просить помощи/надеяться не на кого. Меня поднимут по тревоге и попросят починить сломанное или продиагностировать, что именно не работает. Чтобы быть уверенным в том, что все работает, после релиза в тест и прод я прогонял в ручную набор интеграционных тестов. Около 70. Это занимало немало времени. Это было неудобно, но это нужно было делать.

    Окружение

    IML существует в интеграционном окружении. Часть источников и потребителей - внутрикорпоративные сервисы, другая часть - часть внешние сервисы, которые получают данные за контуром нашей компании. На какие-то источники, я, как сотрудник компании, могу повлиять, написав письмо и получив относительно оперативный результат. На другие источники я имею слабое влияние. Письмо напишу, но вот когда на него ответят не понимаю. Хотя с первыми дело обстоит примерно так же :-). Задача состоит в том, чтобы как можно быстрее определить проблему и запустить исправления. Конечно, можно подождать. Кто-то придет и расскажет, что есть проблемы. Мне хотелось сделать все возможное для оперативного диагностирования возникших проблем.

    Важность регресса в интеграционных тестах

    ILOG ML ничем не отличается от типичной информационной системы. Немного порассуждаю о влиянии регресса на уровнях пирамиды тестирования (ссылки на материалы оставлю ниже). На каждом уровне корректно говорить о понятии регресса. Юнит-тесты, как фундаментальный слой пирамиды помогает с осознанием того, что сделан работающий продукт, выполняющий заложенные в него принципы. Если покрытие юнит-тестами приближается к 100%, это значит, что используемые разработчиками технологии работают так, как было задумано. Интеграционные тесты, на пирамиде, представляют более высокий уровень. Они более затратные и покрывают меньше частей созданной системы, но они выходят за рамки конкретной системы.  Они подтверждают, что интеграционное взаимодействие проходит так, как было оговорено между участниками взаимодействия. Более верхние слои я рассматривать не буду, они не про качество сделанного, они про соответствие сделанного тому, как это планируется использовать.

    Инструмент диагностики

    Интеграционные тесты - крутой инструмент, который помогает поставить диагноз и выявить место возникновения боли. В зависимости от того, как написан наш тест, можем сразу понять причину или потратить на это время. Так, создавая для себя инструмент, мы выходим за границы ответственности своей системы и помогаем диагностировать проблемную ситуацию в интеграционном окружении. Нужен такой инструмент или нет - каждый разработчик или команда решают сами. Множество факторов, которые оказывают влияние на это решение. Мне повезло - у меня была возможность, и я решил ей воспользоваться.

    Документирование контрактов

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

    Интеграционные тесты

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

    RestAssured

    Для выполнения запросов выбрана библиотека Rest Assured (далее RA). Интуитивно понятная в настройке, внедрении, поддержке. Если вы работаете с файлами или требуется сопоставлять хитрые объекты, то в RA есть все необходимое. Чтобы воспользоваться RА, в сборщик (maven) добавляем следующие зависимости:

       <!--rest assured common-->
       <dependency>
           <groupId>io.rest-assured</groupId>
           <artifactId>rest-assured</artifactId>
           <version>${Берем последнюю из доступных}</version>
       </dependency>
       <!--в моем случае тестируется только rest-->
       <dependency>
           <groupId>io.rest-assured</groupId>
           <artifactId>json-schema-validator</artifactId>             
           <version>${Берем последнюю из доступных}</version>         
       </dependency>

Типовая конструкция RA построена на 4 основных методах:

  • Given - Что передаем запросом?

  • When - Каким методом и куда отправляем запрос?

  • Then - Проверяем ответ;

  • Body - Разбираем содержимое переданного ответа;

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

В моем случае типичный кейс был построен так:

/**
 * Настройка подключения для выполнения запросов Rest Assured
 * Настройка для логирования запроса/ответа, если валидация была не успешна
 *
 * @param uri - путь выполнения запроса
 */
public static void initRequestSpecification(String uri) {
    requestSpecification = new RequestSpecBuilder()
            .setContentType(APPLICATION_JSON_VALUE)
            .setBaseUri(uri)
            .build();
 
    // Логирование
    RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
 
    if (RestAssured.filters().isEmpty()) {
        RestAssured.filters(new AllureRestAssured());
    }
}
// Выполняем запрос
given()
        //В запросе должно быть определенное тело
        .body(request)
        //Отправим запрос
        .when()
        //с помощью метода post
        .post()
        //Когда вернется ответ
        .then()
        //проверь
        .assertThat()
        //что статус ответа - 200
        .statusCode(HTTP_OK)
        //контент тип конкретный
        .contentType(APPLICATION_JSON_VALUE)
        //в теле ответа значение поля эквивалентно требуемому значению
        .body("someField", equalTo(response.getSomeField));

Если несколько раз написать такое утверждение оно становится интуитивно понятным. Если вы пишите мок-тесты, то начать использовать/писать интеграционные тесты будет просто. Используемые операторы понятны и очевидны. Суть: выполняем запрос, получаем ответ. Разбираем ответ и проверяем, что полученные значения соответствуют целевым. Часть со сверкой результатов может быть развесистой. Во мне бурлило несогласие - проверять все получаемые значения средствами RA затратно писать и поддерживать. Не было цели переквалифицироваться в профессионального автоматизатора тестирования. Хотелось сделать и в будущем тратить минимальное количество ресурсов, но получать максимальный результат. На помощь пришел родной и дорогой JUNIT. Конструкцию с тестом в большинстве случаев я заменил на такую:

String response = given()
               .body(request)
               .when()
               .post()
               .then()
               .assertThat()
               .statusCode(HTTP_OK)
               .contentType(APPLICATION_JSON_VALUE)
               .body("someField", equalTo(response.getSomeField));
               .extract().asString();          
        
       // С помощью ObjectMapper из ответа формируем строку, с которой будем сверять значение
       String expectedJson =
               objectStaticMapper.writeValueAsString(response);
 
       assertEquals(response, expectedJson);

Какие-то поля, если есть необходимость, можно проверить, но я решил получать ответ и сверять его целиком. В моем случае возникла необходимость собирать/получать request и response. Можно собирать их руками, но мне этого не хотелось. Вылавливать отдельные значения и менять их трудозатратно. Хотелось на вход подать файл и на выходе сверить его с файлом. В этой плане RestAssured немного по-своему обрамляет объект такими литерами '<['. С тем, чтобы разобрать это значение в файл, на ура справился ObjectMapper. Кроме этого нам необходимо вынести инициацию подключения в отдельный метод, который будет выполняться перед тем, как будут выполнены наши тестовые классы. Вот так он будет выглядеть.

// Нужен для преобразования значения ответа в строку
protected static final ObjectMapper objectStaticMapper =
        new ObjectMapper();
 
 
// Нужен для формирования объекта теста
protected static final Gson jsonStaticObject =
        new Gson();
 
//Вот тут используем аннотации Junit 5, чтобы задать поведение тестового класса
@BeforeAll
static void initRequestSpecification() {
    //Устанавливаем соединение        
    requestSpecification = new RequestSpecBuilder()
            .setContentType(APPLICATION_JSON_VALUE)
            .setBaseUri(uri)
            .build();                 
     
    //Устанавливаем логирование для всех выполняемых запросов
    RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
 
    if (RestAssured.filters().isEmpty()) {
        RestAssured.filters(new AllureRestAssured());
 
    }
}
 
// Указываем, что это тест
@Test
// Классификатор для компоновки тестов в финальном представлении
@Story("Какая-то проверка данных")
// Указываем на то, как наш тест будет представлен в отчете
@DisplayName("Успешная загрузка данных")
void universalCheck_audatex_shouldReturnSuccessResult() throws JsonProcessingException {
     
    Request request =
            jsonStaticObject.fromJson(
                    loadJsonData("json/path/data.json"),
                    Request.class);
 
    Response response =
            jsonStaticObject.fromJson(
                    loadJsonData(""json/path/data.json"),
                    Response.class);
 
    String response = given()
            .body(universalCheckRequest)
            .when()
            .post()
            .then()
            .assertThat()
            .statusCode(HTTP_OK)
            .contentType(APPLICATION_JSON_VALUE)
            .body("someField", equalTo(response.getSomeField));
            .extract().asString();
 
    String expectedJson =
            objectStaticMapper.writeValueAsString(universalCheckResponse);
 
    assertEquals(response, expectedJson);
}

Теперь раскроем тему с обработкой запроса/ответа файлами. Я использую Gson и для меня jsonStaticObject это именно он. С помощью метода fromJson, который на вход требует строку с данными и типизацию выходного объекта я получаю сам объект. Метод loadJsonData  - на вход требует адрес ресурса, в котором находится файл с данными, а на выходе мы получаем строку:

import com.google.common.io.Resources;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
 
import java.nio.charset.StandardCharsets;
 
@Slf4j
@UtilityClass
public class ResourceHelper {
 
    /**
     * Загрузить содержимое файла из папки
     *
     * @param resourceFile - путь к файлу
     * @return содержимое файла
     */
    public static String loadJsonData(String resourceFile) {
        String resourceContent = null;
        try {
            resourceContent = Resources.toString(
                    Resources.getResource(resourceFile),
                    StandardCharsets.UTF_8
            );
        } catch (Exception e) {
            log.error("Unable to load resource {}: {}", resourceFile, e.getMessage());
        }
        return resourceContent;
    }
}

То есть, в моем примере, адреса, указанные в тестовом запросе - это пути до файлов, которые хранятся в папке - src/test/resources/сервисКоторыйТестируем/методКоторыйТестируем/типТестовогоКейса/файлСДанными.json (рис.2)

Рис.2
Рис.2

Теперь давайте синхронизируемся. У нас есть инструмент преобразования файлов в запросы, есть инструмент тестирования запросов. Дальше самое сложное. Нужно тренировать пальцы и сделать запросы ко всем сервисам. Наши интеграционные тесты будут состоять из:

  • Модели запроса/ответа;

  • Утилит, которые помогают конвертировать файлы в объекты запроса и ответа;

  • Тестовых наборов для каждого метода;

  • Тестовых кейсов;

  • Чего-то дополнительное, что понадобиться позже ( аутентификация и т.д.);

На каждый метод я создавал минимум 2 кейса:

  • Кейс с обработкой успешного ответа;

  • Кейс с обработкой ошибки;

Кейсы различались статусами и содержимым ответов. Тестовый набор для каждого метода обернут в отдельный класс. После того, как я начал писать второй тестовый класс стало понятно что у меня есть параметры, которые будут мигрировать из класса, в класс. Это используемый Gson объект, ObjectMapper и параметры подключения к сервису. Вынесем их в отдельный класс, от которого наследуем все наши последующие кейсы в рамках проверки конкретного сервиса:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import io.qameta.allure.restassured.AllureRestAssured;
import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import org.junit.jupiter.api.BeforeAll;
 
import static io.restassured.RestAssured.requestSpecification;
import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
 
public class Settings {
 
    protected static final ObjectMapper objectStaticMapper =
            new ObjectMapper();
 
 
    protected static final Gson jsonStaticObject =
            new Gson();
 
    @BeforeAll
    static void setRequestSpecification() {
 
        requestSpecification = new RequestSpecBuilder()
                .setContentType(APPLICATION_JSON_VALUE)
                .setBaseUri(uri)
                .build();
 
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
 
        if (RestAssured.filters().isEmpty()) {
            RestAssured.filters(new AllureRestAssured());
        }
    }
 
}

После того как мы вынесем логику подключения к сервису в отдельный класс и сделаем кейсы на проверку метода, наш типичный тестовый класс, покрывающий тестовый метод, будет выглядеть так:

import com.fasterxml.jackson.core.JsonProcessingException;
import io.qameta.allure.Epic;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import ru.alfastrah.odm.integrationtests.model.universalcheck.UniversalCheckRequest;
import ru.alfastrah.odm.integrationtests.model.universalcheck.UniversalCheckResponse;
 
import static io.restassured.RestAssured.given;
import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
 
// Какой сервис тестируем. Аннотация нужна для последующего формирования отчетности
@Epic(UNIVERSAL_DESCRIPTION)
// Какой функционал/метод тестируем. Аннотация нужна для последующего формирования отчетности
@Feature("Поиск данных Audatex")
class MethodUnderTest extends Settings {
 
    @Test
    @Story("Что тестируем")
    @DisplayName("Успешный кейс")
    void service_method_Result() throws JsonProcessingException {
 
        Request request =
                jsonStaticObject.fromJson(
                        loadJsonData("json/path/data.json"),
                        Request.class);
 
        Response response =
                jsonStaticObject.fromJson(
                        loadJsonData("json/path/data.json"),
                        Response.class);
 
        String response = given()
                .body(request)
                .when()
                .post()
                .then()
                .assertThat()
                .statusCode(HTTP_OK)
                .contentType(APPLICATION_JSON_VALUE)
                .body("Field", equalTo(response.Field()))
                .body("structureList[0].someField", equalTo(response.someField()))
                .body("structureList[0].someAnotherField", equalTo(response.someAnotherField()))
                .extract().asString();
 
        String expectedJson =
                objectStaticMapper.writeValueAsString(response);
 
        assertEquals(response, expectedJson);
    }
 
    @Test
    @Story("Что тестируем")
    @DisplayName("Не успешный кейс")
    void service_method_Exception() throws JsonProcessingException {
 
        Request request =
                jsonStaticObject.fromJson(
                        loadJsonData("json/path/data.json"),
                        UniversalCheckRequest.class);
 
        Response response =
                jsonStaticObject.fromJson(
                        loadJsonData("json/path/data.json"),
                        UniversalCheckResponse.class);
 
        String response = given()
                .body(request)
                .when()
                .post()
                .then()
                .assertThat()
                .statusCode(HTTP_BAD_GATEWAY)
                .contentType(APPLICATION_JSON_VALUE)
                .body("Field", equalTo(response.Field()))
                .body("structureList[0].someField", equalTo(response.someField()))
                .body("structureList[0].someAnotherField", equalTo(response.someAnotherField()))
                .extract().asString();
 
        String expectedJson =
                objectStaticMapper.writeValueAsString(response);
 
        assertEquals(response, expectedJson);
 
    }
 
}

Story → Feature → Epic

Мы разобрались с тем, как должен выглядеть набор с тестами, проверяющий конкретный метод. Конкретная проверка метода - стори. Несколько стори в совокупности, образуют фичу, которую мы проверяем. Фича = проверяемый метод. Фича делается в каком-то сервисе. Расстановка этих аннотаций приведена у меня в примере выше. Ее не следует игнорировать. Аннотации нужны. Они уже от инструмента визуализации.  Как мы их расставим, определит вид отчета о выполнении тестов. Мы подошли к другому инструменту. О нем поговорим немного ниже. Основное, что хочется донести - именуйте свои классы и методы аннотациями, как предписывает теория тестирования.

Типичный Suite → Типичный Epic

Выше мы упаковали конкретный класс, проверяющий конкретный метод. Ничего не мешает, если логика ветвится или есть необходимость, провести проверку не только успешного ответа, но и всех исключений, которые мы можем получить. Чтобы не наводить беспорядок, который имеет свойство плодиться, я рекомендую придерживаться концепции "1 метод, 1 класс". Так, у меня всегда в конкретном месте будет ответственность проверки конкретного сервиса.

Визуализация результатов → AllureReport

Про визуализацию результатов мы упомянули. Сейчас поговорим подробнее. В моем проекте, за визуализацию отвечает инструмент allure report. Ниже оставлю ссылки на официальную документацию. Это очень мощный инструмент визуализации результатов. В нашем случае, мы просто достаем получаемый отчет после прогона теста из папки и публикуем его возможностями gitlab pages. Приведенные выше аннотации @Epic, @Feature, @Story, @DisplayName - это именно аннотации allure. Его настройка займет какое-то время. Во-первых - зависимости:

<!--Модуль взаимодействия allure c rest-assured-->
<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-rest-assured</artifactId>
    <version>${Берем последнюю из доступных}</version>
</dependency>
<!--Движок allure. Как правило используется или junit или testNG-->
<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-junit5</artifactId>
    <version>${Берем последнюю из доступных}</version>
    <scope>test</scope>
</dependency>
<!--Модуль взаимодействия allure c maven-->
<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-maven</artifactId>                
    <version>${Берем последнюю из доступных}</version>                   
</dependency>

Следом нужно добавить отдельный файл - allure.properties, в котором вы задаете настройки отчета. Основное - куда его помещать после формирования. В моем случае настройка такая:

allure.results.directory=target/allure-results

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

Переменные, профили Maven и исполнение тестов

Проект почти готов. Его можно использовать, но есть несколько оговорок. Инициализируя проект, мы передаем конкретный путь до сервиса. В ситуации, когда у Вас путь один, то есть Вы реализуете интеграционное тестирование только для конкретной среды - задача решена. В ситуации, когда Вы хотите извлечь из тестов максимум и использовать их не на одной, а на нескольких средах, Вам важно иметь возможность передавать разные пути. Если бы мы использовали spring/spark/microunat/quarkus, то такой проблемы не было бы. Мы пользовались стандартными возможностями spring/.., хранили переменные в application.yaml/application.properties/bootstrap.yaml/bootstrap.properies и использовали config-server, zookeper или что-то еще. Но я напомню, что у нас в проекте нет spring или чего-то подобного. Как в таком случае отделить код от конфигурации и сделать приложение более универсальным, чем набор тестов для конкретной среды исполнения? 

Профили maven

На помощь придут профили maven. Они задуманы как инструмент, который позволяет настроить пути к локальной файловой системе или указать разный набор зависимостей для разных условий сборки проекта. Профили настраиваются с помощью элементов, доступных в самом POM. Для этого требуется добавить дополнительный раздел. Профили изменяют POM во время сборки и предназначены для использования в дополнительных наборах для предоставления эквивалентных, но разных параметров, для разных целевых сред. При корректном использовании профилей увеличивается гибкость сборки и управления проектами. 

Что нужно для работы с профилем

Для меня было важным не внося дополнительных настроек, запускать проект и выполнять тесты на конкретной среде. К переменным профиля у меня относятся адреса сервисов, по которым проводится интеграционное тестирование. То есть, выполнив команду для определенной среды, в коде должен считываться актуальный профиль, параметры из профиля передаваться в системный компонент, который предоставляет данные для теста. Начнем с создания профилей. У меня их три - для локального запуска, для запуска в тестовой среде и для запуска в продуктивной среде. Профили создадим в папке - src/main/resources/env. Но проект с интеграционными тестами не должен ничего знать про разные настройки. Он должен работать с конкретными свойствами, из которых должен получать значения. В проекте не хватает общего файла-обертки, через который будет считываться информация в коде. Создадим общий файл - config.properties, который положим чуть выше в директории (Рис.3).

Рис.3
Рис.3

Профиль для работы с конкретной системой содержит данные подключения:

#ServiceName
serviceUrl=http://localhost:8080/

В общем профиле содержится информация по передаваемым переменным:

#ServiceName
serviceUrl=${serviceUrl}

Настроим maven для работы с созданными файлами. В специальном блоке <profiles> создадим профили, которые мы будем использовать для запуска и исполнения проекта. Создадим так, чтобы у каждого профиля была переменная среды <env>, которая будет соответствовать значению, определенном в имени файла со свойствами - config.${env}.properties. Для локального профиля укажем, что он будет профилем по умолчанию.

<profiles>
        <profile>
            <id>integration-test-local</id>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
            <properties>
                <env>local</env>
            </properties>
        </profile>
        <profile>
            <id>integration-test-test</id>
            <properties>
                <env>test</env>
            </properties>
        </profile>
        <profile>
            <id>integration-test-prod</id>
            <properties>
                <env>prod</env>
            </properties>
        </profile>
    </profiles>

Так у нас получился профиль для использования в каждой системе. Чтобы управлять профилем в maven необходимо установить флаг -P. Если флаг не передать, работа будет выполняться с профилем по умолчанию. Чтобы маven выводил значения используемых профилей нам нужно в build секцию добавить еще один плагин.

<plugin>
     <groupId>org.apache.maven.plugins</groupId>
     <artifactId>maven-help-plugin</artifactId>
     <version>${${Берем последнюю из доступных}}</version>
     <executions>
         <execution>
             <id>show-profiles</id>
             <phase>compile</phase>
             <goals>
                 <goal>active-profiles</goal>
             </goals>
         </execution>
     </executions>
 </plugin>

Посмотрим, какие профили доступны в проекте. На вход команда mvn package - на выходе получаем следующий результат (Рис.4)

Рис.4
Рис.4

Первый профиль, настроенный в файле settings.xml maven. Второй профиль - тот, который выше мы настроили по умолчанию. Теперь надо настроить передачу параметров в приложение. В блоке <build> настроим ресурс, из которого будем получать наши свойства.

<resources>
      <resource>
          <directory>src/main/resources</directory>
          <filtering>true</filtering>
          <includes>
              <include>*.properties</include>
          </includes>
      </resource>
  </resources>

Теперь настроим фильтрацию данных по свойствам среды, который должны передаваться в ресурс при запуске maven. Это делается так-же в блоке <build> за счет использования тега <fuilter>, в котором мы укажем файл, из которого нужно брать переменные. В профилях мы задали собственный тег <env> по нему и будем фильтровать нужный нам файл с свойствами.

<filters>
    <filter>src/main/resources/env/config.${env}.properties</filter>
</filters>

Следом нужно обработать параметры, передаваемый из файлов со свойствами. Лезем в документацию maven и читаем.

Раздел <properties> позволяет указать пары ключ-значение в свободной форме, которые будут включены в процесс интерполяции POM.

Создаем класс, который должен соответствовать параметрам файла со свойствами. У нас пока один параметр, поэтому наш класс со свойствами будет очень кратким.

Ну и класс-обработчик, который мы будем использовать в коде, для работы с параметрами.

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import ru.alfastrah.odm.integrationtests.model.configproperty.Property;
 
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
 
@Slf4j
@UtilityClass
public class PropertyWrapper {
    private static final ObjectMapper objectMapper
            = new ObjectMapper();
 
    /**
     * Получить значения из config.properties
     *
     * @return Property объект
     */
    public static Property getProperty() {
        log.info("Load properties file config.properties");
        return objectMapper.convertValue(
                loadPropertiesFromFile("config.properties"),
                Property.class);
    }
 
    /**
     * Загрузить содержимое файла
     *
     * @param fileProperties - название файла с properties
     * @return содержимое файла
     */
    private static Properties loadPropertiesFromFile(String fileProperties) {
 
        Properties properties = new Properties();
 
        try (InputStream resourceAsStream =
                     PropertyWrapper.class.getClassLoader()
                             .getResourceAsStream(fileProperties)) {
            properties.load(resourceAsStream);
        } catch (IOException e) {
            log.error("Unable to load properties file : {} ", fileProperties);
        }
        return properties;
    }
 
}

Итого: мы создали профили, добавили их обработку. Самое время проверить, как чувствуют себя наши тесты с разными профилями. Тесты мы будем исполнять командой verify, так же подойдет команда test. Verify я использую потому, что она мне больше нравится дополнительными проверками. Запускаем maven, компилируем проект, запускаем тесты с определенным профилем и смотрим за результатом (Рис.5)

Рис.5
Рис.5

Финал - для управления проектом будем использовать следующие команды:

* mvn compile -P integration-test-local verify -> используем локальный профиль;
* mvn compile -P integration-test-test verify -> используем тестовый профиль;
* mvn compile -P integration-test-prod verify -> используем продуктивный профиль;

Встраивание в процесс разработки

У нас готов проект. Теперь нужно настроить его для использования в разных средах. Приводить тут скрипты ci/cd для прогона тестов я не буду. Тут тоже нет ничего интересного. Мы добавили запуск тестов в pipeline → при раскате в дев среду запускаются интеграционные тесты с профилем дев, при раскате в мастер - запускается профиль прод. При локальной разработке я использую локальный профиль. Он помогает при рефакторинге сервисов. Придает уверенности. Настроим scheduler, который запускает тесты 2 раза в день и на странице gitlab pagesможно будет увидеть результаты последнего прогона (Рис. 6)

Рис.6
Рис.6

Результат сортировки отчета по добавленным нами тегам мы можем увидеть на вкладке behaviors (Рис.7)

Рис.7
Рис.7

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

Кроме того, с помощью pipeline sсhedules были добавлены 2 job для запуска прогона тестов на разных средах по необходимости (Рис.8)

Рис.8
Рис.8

Вместо завершения и ссылка на репозиторий

Проект с интеграционными тестами только создан. Мыслей про то, как его можно развивать и дополнять много. Во-первых - я покрыл только один сервис. В этой части предстоит большая работа по покрытию всего остального. В ходе этой работы станет понятно, что упущено и что стоит добавить. Из того, что уже находится в зоне размышления - научиться работать с хранилищем секретов vault, чтобы подключаться к сервисам, требующим авторизации и не указывать в явном виде логин/пароль/токен. Дорогу осилит идущий. Я уверен, на этом пути предстоит еще много открытий)

Благодарности

Спасибо Юре, Даше, Александре, Александру, Диме за помощь и наставления:

  • Юра - ты открыл для меня профили, как технологию;

  • Даша - ты зародила во мне огонек автоматизатора тестирования;

  • Александра - ты дала возможность сделать это;

  • Никита - ты покритиковал написанное и предложил много вещей, о которых мне нужно подумать;

  • Александр - провел ревью сделанного и написанного;

  • Дима - сделал мой русский язык, по настоящему русским;

    Ссылка на репозиторий

    Репозиторий с обезличенным проектом - тут (пока проходила работа над статье, добавились классы и методы для работы с vault и keycloak);

    Полезные ссылки и литература

    О чем 

    Ссылка

    Зачем это читать

    1

    Пирамида тестирования

    Ссылка

    Разобраться в уровнях тестирования поглубже

    2

    Настройка и использование профилей maven

    Ссылка

    Возможности maven безграничны и разбираться в них будет не лишним )

    3

    Профили maven

    Ссылка

    Официальная документация

    4

    Rest Assured на русском

    Ссылка

    Познакомиться на возможности инструмента

    5

    Rest Assured. Примеры настройки и внедрения

    Ссылка

    Взять и начать использовать

    6

    Rest Assured

    Ссылка

    Официальная документация

    7

    Allure Report

    Ссылка

    Официальная документация

    8

    Статья-разбор аннотаций Allure

    Ссылка

    Очень подробный гайд по аннотациям Allure

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии7

Публикации

Информация

Сайт
alfastrah.ru
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия