Всем привет! Я Максим, QA, автоматизатор, TestOps’тизатор, жнец, хлебец, на дуде игрец. У меня есть довольно интересный опыт построения автоматизации с нуля, и я решил им поделиться. Попытаюсь изложить историческую канву, личный опыт, описать, какие инструменты внедрял и почему, сколько это стоило в часах и каких результатов достиг.

Дисклеймер

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

Статья разделена на две части. В первой больше упор на разное кунг-фу с Java. Вторая часть больше про докер и БД.

Словарик

Некоторые термины могут отличаться в рамках ЯП. Во избежание недопонимания ниже небольшой словарик:

  • Смаршалить/Анмаршалить — иначе говоря, сериализовать/десериализовать Java-объект в строку/файл/XML/JSON и обратно.
  • POJO (Plain Old Java Object) — простой Java-объект, не зависящий от специфических фреймворков и не расширяющий заранее заданные классы. Обычно используется для упаковки и переноса данных.
  • АТ — платформа автотестов, которая крутит тесты и которую я разрабатываю.
  • ОТ — объект тестирования, или тестируемое приложение.

Вводные

Что дано? 10-летний легаси Java-зверь с тонной функционала (в дальнейшем — ОТ), а именно:

  1. SOAP API, которое за 10 лет никто толком не шатал.
  2. REST API для работы с web.
  3. Лицензии, которые меняют функционал платформы.
  4. Разные конфиги, которые тоже меняют функционал платформы.
  5. SNMP, SFTP, JMX-функционал.
  6. Платформа активно работает с сетевыми протоколами, что равно дампы-дампы-дампы.

Ситуация не сахар, а еще и ушел Димка, но что же есть на старте? Коллекция в POSTMAN на 130 запросов с прикрученной JS-автоматизацией, и крутится это в NEWMAN. В целом неплохо, но хочется что-то масштабнее, чтобы и поддерживалось легко, и можно было 100–500 тестов написать, а оно все еще легко поддерживается, и инструменты разные, и FRIDGE, SWITCH, WASHING MACHINE…

Чуть раскрою тему POSTMAN, SOAP UI и прочих тулз-браузеров CURL. Подробно расписывал тут. Если обобщить, в POSTMAN каждое тело запроса хранится отдельно, захардкожено, в большом JSON вместе с JS-автоматизацией, хедерами и прочим. Для 130 запросов JSON-файл был объемом 10 тысяч строк, а теперь представьте, какого объема будет JSON-файл для 5 тысяч запросов, а для 15 тысяч? Этот JSON-файл нам нужен для прогона в NEWMAN, этот JSON-файл нам криво-косо поддерживать в GitLab (ведь CI-раннер все берет из гитовой репы, а не с вашего личного ПК).

Максим Братковский
Максим Братковский
Linux expert
Задать вопрос
Звучит не очень удобно. Главная проблема этого подхода — стоимость поддержки. Чем больше запросов в коллекции, тем больше времени она будет занимать. В свою очередь, подход, который я опишу ниже, позволяет, грубо говоря, переиспользовать эту XML, например:
<ns2:createCampaign>
    <name>...</name>
    <transportLinkId>100</transportLinkId>
    <checkSimLocation>false</checkSimLocation>
    <allowLocationOnMatch>false</allowLocationOnMatch>
    <startDate>2026-06-24</startDate>
    <finishDate>2026-07-01</finishDate>
    <events>
        <day>1</day>
        <fromTime>00:00</fromTime>
        <toTime>23:59</toTime>
        <speed>500</speed>
    </events>
    <events>
        <day>2</day>
        <fromTime>00:00</fromTime>
        <toTime>23:59</toTime>
        <speed>500</speed>
    </events>
    <events>
        <day>3</day>
        <fromTime>00:00</fromTime>
        <toTime>23:59</toTime>
        <speed>500</speed>
    </events>
    <events>
        <day>4</day>
        <fromTime>00:00</fromTime>
        <toTime>23:59</toTime>
        <speed>500</speed>
    </events>
    <events>
        <day>5</day>
        <fromTime>00:00</fromTime>
        <toTime>23:59</toTime>
        <speed>500</speed>
    </events>
    <events>
        <day>6</day>
        <fromTime>00:00</fromTime>
        <toTime>23:59</toTime>
        <speed>500</speed>
    </events>
    <events>
        <day>7</day>
        <fromTime>00:00</fromTime>
        <toTime>23:59</toTime>
        <speed>500</speed>
    </events>
    <steps>
        ...
    </steps>
    <imsis>...</imsis>
</ns2:createCampaign>

1.5 тысячи раз, меняя только значение imsi, steps, name. Вызов XML происходит из:

public static class OtaCreateCampaignRequestBean {

    public static OtaCreateCampaignRequest getCampaign(){
        return new OtaCreateCampaignRequest()
                .withStartDate(CommonEntities.CURRENT_DATE.format(CommonEntities.FORMATTER))
                .withFinishDate(CommonEntities.CURRENT_DATE.plusWeeks(1).format(CommonEntities.FORMATTER))
                .withEvents(getEvents(7, "00:00", "23:59", 500));
    }

    public static CampaignStep getStep(int count, CampaignStepType step, String data){
        return new CampaignStep().withStep(count).withType(step).withOperationData(data);
    }

    public static List<CampaignEvent> getEvents(int days, String fromTime, String toTime, int speed){
       return IntStream.rangeClosed(1 , days)
               .mapToObj( i -> new CampaignEvent().withDay(i).withFromTime(fromTime).withToTime(toTime).withSpeed(speed))
               .toList();
    }
}

Это место, к которому обращаются все тесты, связанные с этой XML. Если я захочу изменить что-то общее у всех тестов, я приду сюда, изменю пару параметров — и готово, а не буду менять 1.5 тысячи XML.

Уверен, найдутся энтузиасты, которые смогут провернуть подобное и в POSTMAN — через указание XML в env, вызов этой env перед запросом, обработку через JS, подкладывание собранного тела в запрос и т.д. Тут скажу лишь: каждый дрочит как хочет делает как считает нужным.

Определяем требования

  1. ТК ОТ на Java, в компании все жабущники, значит, АТ пилим тоже на Java. Продать идею Python-проекта сложно в таких обстоятельствах (да и не хочется).
  2. На стадии MVP ожидается около 700 тестов, это около 1.5 тысяч SOAP-запросов на ОТ.
  3. XML для SOAP-запросов должны собираться из кода. Никаких захардкоженных файлов. Со 130 было тяжело, с 1.5 тысяч будет хуже…
  4. Allure-отчеты, обязательно.
  5. Нужно будет прикрутить всякие SNMP, SFTP, SSH, JMX-клиенты.
  6. Различные генераторы данных. Мы чо, не автоматизаторы????
  7. Возможность собирать XML не только для SOAP, но и как документ для загрузки на ОТ.
  8. Ну и прочие финтифлюшки по мелочи.

MVP

Самым сложным на стадии MVP был 3-й пункт требований. SOAP в полуживом состоянии, все инструменты и документация от Apache, в основном. Готовых решений и мануалов — кот наплакал. После сношений с гуглом, ИИ, StackOverflow я и Бадди (в дальнейшем Андрюха) находим его — CXF.

Фреймворк для работы с SOAP, есть все нужные либы — JAX-WS, JAXB, JAX и прочие, но что для нас было важно?

  1. Умеет генерировать POJO из WSDL-проекта.
  2. Собирает SOAP-клиент и отправляет запросы.
  3. Генерирует объектную фабрику (JAXB) и прочие штуки.

Что это дает?

  1. Не надо писать вручную кучу-прекучу POJO и поддерживать их.
  2. Этот же фреймворк используется в ОТ. Совместимость 100%.

Чуть подробнее об этом я писал тут. Здесь же опишу те проблемы, с которыми столкнулся при использовании вышеописанного функционала:

  1. Боль моя — дырка задница при работе с Apache-документацией.
  2. JAX-WS, сервис, который отправляет SOAP-запросы, не интегрирован с Allure, да и в логирование запросов не особо умеет.

Если с первым ничего не поделать — расслабиться и получать удовольствие, то второе надо решать, пункт требований 4.

Пилим интеграшку в Allure Report для JAX-WS

Как решили? Прокси-сервис! (Внимание!!! Ниже будет Java-код, беременных или впечатлительных — уберите от экранов)

/**
 * Proxy-сервис, позволяющий добавить бизнес-логику перед выполнением запроса, при этом не затрагивая JAX-WS.
 * @author m.bratkovksii
 */
public class UserHandlerCapableServiceProxy {
     public interface UserHandler {
        default void beforeInvocation(HashMap<Object,Object> ctx, Method m, Object[] args) {}
        default void handleResult(HashMap<Object,Object> ctx, Object result, Method m, Object[] args) {}
        default void handleException(HashMap<Object,Object> ctx, Throwable t, Method m, Object[] args) {}
    }


     private record GenericInvocationHandler(Service realService, List<UserHandler> handlers) implements InvocationHandler {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            HashMap<Object,Object> ctx = new HashMap<>();

            this.handlers.forEach(h -> h.beforeInvocation(ctx, method, args));
            try {
                final var res = method.invoke(realService, args);
                this.handlers.forEach(h -> h.handleResult(ctx, res, method, args));
                return res;
            } catch (Exception e) {
                this.handlers.forEach(h -> h.handleException(ctx, e, method, args));
                throw e;
            }
        }
    }
    /**
     * Обертка, необходимая, чтобы добавить прокси-поведение.
	 * @param s Порт, используемый для отправки запросов
     * @param handlers список обработчиков
     * @return Прокси-порт, используемый для отправки запросов
     */
     public static Service wrap(Service s, List<UserHandler> handlers) {
        return (Service) Proxy.newProxyInstance(Service.class.getClassLoader(),
                new Class<?>[] { Service.class },
                new UserHandlerCapableServiceProxy.GenericInvocationHandler(s, handlers));
    }
}

Что делает? Оборачиваем SOAP-клиент в прокси, докидываем хендлеры. Перед отправкой запроса выполнится что-то, что мы добавили, после обработается результат, и если ошибка — тоже накинется дополнительное поведение.

Пример хендлера:

/**
 * Handler для добавления XML-тела Service-запроса в Allure-отчет.
 */
 public class AllureHandler implements UserHandlerCapableServiceProxy.UserHandler{

 public static final AllureHandler ALLURE_HANDLER = new AllureHandler();

 private JaxbElementContext jaxbElementContext;

    @Override
    public void beforeInvocation(HashMap<Object, Object> ctx, Method m, Object[] args) {
        jaxbElementContext = (JaxbElementContext) ctx.computeIfAbsent("xml", k -> new JaxbElementContext(m, args[0]));

        Allure.step(m.getName());
        Allure.addAttachment("Request " + m.getName() , "text/plain", jaxbElementContext.getRequestBody());
    }

    @Override
    public void handleResult(HashMap<Object, Object> ctx, Object result, Method m, Object[] args) {
        Allure.step(m.getName() + " result");
        Allure.addAttachment("Result " + m.getName(), "text/plain", jaxbElementContext.getResponseBody(result));
    }

    @Override
    public void handleException(HashMap<Object, Object> ctx, Throwable t, Method m, Object[] args) {
        if (t instanceof InvocationTargetException){
            var e = ((InvocationTargetException) t).getTargetException();
            Allure.step(m.getName() + " exception");
            Allure.addAttachment("Exception", "text/plain", jaxbElementContext.getExceptionBody(e));
        }
    }
}

Такой же можно использовать и для логирования.

Маршалинг/Анмаршалинг

Вторая задача: надо прикреплять в Allure красивое тело запроса, чтобы понимать, что ушло и что пришло. Чтобы превратить Java-объект в текстовый XML, его нужно смаршалить. Вот несколько решений:

  1. Самое дешевое — подключить Jackson в Java (или любой другой аналог в вашем ЯП). Действует просто, не требует жесткого фикса типа объекта, XML генерит тоже простые:
	   <object>
		<param1>...<param1>
		   ...
	   </object>

Если вам этого достаточно, смело пользуйте. Доку найдете в интернетах.

  1. Чуть более fancy — маршалинг через JAXB. Эта либа уже есть в проекте, идет в базе CXF и делает такую красоту:
	<ns2:methodName xmlns:ns2="http://url2uHost">
		 <param>...</param>
			...
	</ns2:methodName>

Буквально, если добавить этот текст в это:

<?xml version='1.0' encoding='UTF-8'?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
	<S:Body>
		...
	</S:Body>
</S:Envelope>

вы получите то, что буквально едет по HTTP. НО! Есть нюансы :)

Пляски с JAXB (Java)

Если вам повезло и вы счастливчик, и в вашем WSDL указано, что является @XmlRootElement, — поздравляю, вы избежите многих танцев с бубном. Достаточно сделать контекст JAXB на ObjectFactory, который генерируется из вашего WSDL CXF (как сделать маршалер, смотрите в интернете, документов полно):

JAXB_CONTEXT = JAXBContext.newInstance(ObjectFactory.class);
Marshaller marshaller = JAXB_CONTEXT.createMarshaller();
Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();

Этого будет достаточно, чтобы JAXB сам искал себе все контексты.

Если @XmlRootElement нет — добро пожаловать в клуб. На просторах интернета витают слухи, что с помощью биндингов можно заставить JAXB добавлять @XmlRootElement для каждого POJO при генерации. Я так и не разобрался, поэтому написал свой велосипед.

Основная суть: CXF помимо POJOs генерирует ObjectFactory, в ней лежат методы создания как POJO, так и JAXBElement. Второй нам и нужен, чтобы смаршалить POJO без @XmlRootElement в рамках JAXBContext.newInstance(ObjectFactory.class). Чтобы не писать некий switch, который для каждого POJO дергает нужный метод из ObjectFactory, эта тулза рефлективно дергает нужный метод для каждого POJO.

public class ObjectFactoryReflectService {
    @SuppressWarnings("unchecked")
    public static <T> JAXBElement<T> reflectObjectFactoryForWebMethod(String methodName, T object) {
        String jaxbMethodName = "create" + methodName.substring(0, 1).toUpperCase() + methodName.substring(1);

        try {
            // Ищем метод, который принимает объект типа T
            Method method = ObjectFactory.class.getMethod(jaxbMethodName, object.getClass());

            // Вызываем метод на objectFactory, передавая object как параметр
            return (JAXBElement<T>) method.invoke(OBJECT_FACTORY, object);
        } catch (Exception e) {
            throw new RuntimeException("Failed to create JAXBElement for " + methodName, e);
        }
    }

    @SuppressWarnings("unchecked")
    public static <T> JAXBElement<T> reflectObjectFactoryForExceptions(T object) {
        String jaxbMethodName = "create" + object.getClass().getSimpleName();

        try {
            // Ищем метод, который принимает объект типа T
            Method method = ObjectFactory.class.getMethod(jaxbMethodName, object.getClass());

            // Вызываем метод на objectFactory, передавая object как параметр
            return (JAXBElement<T>) method.invoke(OBJECT_FACTORY, object);
        } catch (Exception e) {
            throw new RuntimeException("Failed to create JAXBElement for " + object.getClass().getSimpleName(), e);
        }
    }

    public static Object reflectThrowableConverter(Throwable target) {
        try {
            Method getFaultInfoMethod = ReflectionSupport.findMethod(target.getClass(), "getFaultInfo")
                    .orElseThrow(() -> new IllegalStateException("getFaultInfo method not found"));

            Object faultInfo = ReflectionSupport.invokeMethod(getFaultInfoMethod, target);

            ReflectionSupport.findMethod(faultInfo.getClass(), "setMessage", String.class)
                    .ifPresent(setMessageMethod ->
                            ReflectionSupport.invokeMethod(setMessageMethod, faultInfo, target.getMessage()));

            return faultInfo;

        } catch (Exception e) {
            throw new IllegalStateException("Failed to handle throwable: " + target.getClass().getSimpleName(), e);
        }
    }
}

Это решение может показаться хрупким, но на деле показывает себя стабильно. На текущий момент в 2.7 тысячи тестов — примерно 5 тысяч запросов, а это 10 тысяч срабатываний ObjectFactoryReflectService. Ни разу не поломалось.

SOAP-клиент (Java)

Можно реализовать разными способами. Как пример, CXF сгенерирует вам класс Client в демонстрационных целях. Я взял клиент, который написали для юнит-тестов основного приложения.

Основная суть: CXF берет ваш WSDL, а конкретно строки:

<wsdl:service name="YouService">
	<wsdl:port binding="tns:YouServiceSoapBinding" name="servicePort">
		<soap:address location="http://youPath"/>
	</wsdl:port>
</wsdl:service>

И генерирует из них два класса:

  1. public class YouService extends jakarta.xml.ws.Service, который хранит адреса, пути, инициализацию Service.
  2. Сам Service, который содержит запросы, поддерживаемые SOAP-сервером. Пример:
@WebService(targetNamespace = "http://youPath", name = "service")
@XmlSeeAlso({ObjectFactory.class})
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface Service {

    @WebMethod
    @WebResult(name = "expectedResult", targetNamespace = "http://youPath", partName = "result")
    public NeededObject makeSomeSpecial(

        @WebParam(partName = "NeededObject", name = "NeededObject", targetNamespace = "http://youPath")
        NeededObject nameObject
    ) throws Declared_Exceptions;

И сборка самого клиента:

public class ClientFactory {
//    Данные пользователя захардкожены, пока не понадобилось обратное
    private static final String USERNAME = "login";
    private static final String PASS = "pass";

    private static final Service SERVICE = new YouService()
            .getServicePort();

    static private Service port;

    public static Service getClient(){
        if (port == null){
            initClient();
            //та самая обертка
            port = UserHandlerCapableServiceProxy.wrap(port, List.of(AllureHandler.ALLURE_HANDLER, LogHandler.LOG_HANDLER));
        }
        return port;
    }

    /**
     * Инициализирует клиент
    */
    private static void initClient() {
        initSSLContext();

        ((BindingProvider) SERVICE)
                .getRequestContext()
                .put(MessageContext.HTTP_REQUEST_HEADERS, authorizationHeader(USERNAME, PASS));

        port = SERVICE;
    }

    /**
     * Хранит все необходимые HTTP-сертификаты
     */
     private static final TrustManager[] ALL_ACCEPTING_TRUST_MANAGER = new TrustManager[]{new X509TrustManager() {

        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }

        @Override
        public void checkServerTrusted(X509Certificate[] certs, String authType)
                throws CertificateException {
        }

        @Override
        public void checkClientTrusted(X509Certificate[] certs, String authType)
                throws CertificateException {
        }
    }};

    /**
     * Инициализирует SSL, TLS-контексты
    */
    private static void initSSLContext() {
        try {
            SSLContext ctx = SSLContext.getInstance("TLS");
            ctx.init(null, ALL_ACCEPTING_TRUST_MANAGER, new SecureRandom());
            SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();
            HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory);
            HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Инициализирует базовую аутентификацию
	*/
     private static Map<String, List<String>> authorizationHeader(String username, String password) {
        final var credentials = Base64
                .getEncoder()
                .encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));

        return Map.of("Authorization", List.of("Basic " + credentials));
    }
}

И далее можно спокойно слать запросы на сервер таким образом:

port = getPort();
port.makeSomeSpecial(NeededObject);

И тут главный кайф: не нужно париться над хедерами, коннектом, сборкой XML — JAX-WS все сделает сам. Собрал порт, положил объект, отправил. Ляпота!

З.Ы. Если идти этим путем, рекомендую подключать либы не самостоятельно, а через Jakarta BOM — сэкономит кучу времени и нервов.

Альтернатива

В целом, с инструментами, описанными выше, можно собрать Rest Assured. У этого решения есть свои плюсы:

  1. Логирование из коробки.
  2. Allure из коробки.
  3. Общепринятый HTTP-клиент, бест-практис, имеет множество альтернатив на разных ЯП.
  4. Один клиент и на REST, и на SOAP.

Минусы:

  1. Придется посношаться с хедерами и прочим.
  2. Нужно придумать, как добавлять тело запроса в <S:Envelope/><S:Body/>.

Почему я так не сделал в своем проекте? Когда я все это поднимал, это просто не казалось возможным. Буквально, CXF парсит WSDL и собирает все сущности → собирается порт → отправляется запрос → получаем ответ. Все подробности скрыты внутри JAX-WS. Только после сильного погружения в документацию и сборки ObjectFactoryReflectService стало понятно, что можно это засунуть и в RestAssured, но уже было написано слишком много тестов, чтобы мигрировать их в RestAssured. Работает и работает…

JUnit

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

@ParameterizedTest

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

Пример: в рамках такого клочка кода реализовано порядка 70 тестов, в которых 140 запросов (создаем сущность, чекаем сущность):

	@ParameterizedTest(name = "{index}: {0}")
    @DisplayName("Create Campaign with FCP")
    @CsvFileSource(resources = "/csv/rfm_commands.csv")
    public void testFCP(CampaignStepType step) throws Exception{
        var body = getCreateCampaignRequestBodyForRfmOrLoad(step, FCP);
        long idCampaign = port.createCampaign(body);
//      Инициируем получение статистики последней созданной кампании
        Campaign chekCampaign = getLastCampaign();
//      Ассерты
        assertEquals(idCampaign, chekCampaign.getId());
        assertEquals(body.getTransportLinkId(), chekCampaign.getTransportLink().getId());
        assertEquals(body.getSteps().get(0).getType(), chekCampaign.getSteps().get(0).getType());
    }

Самое главное: такие тесты максимально просто поддерживать и масштабировать. Я реализовал 2 тысячи+ тестов на примерно похожем коде в разных его вариациях. Поддержка этих тестов за полгода заняла пару часов.

Параметризированные тесты могут принимать параметры из CSV с помощью @CsvFileSource() (пример выше — отдельный кайф, JUnit сам приводит текст к типу данных). @MethodSource() в отдельном методе накидывает генерацию тестовых данных, типа:

@MethodSource("getValue")
@Test
...

private static Stream<Arguments> getValue(){
    return Stream.of(
            Arguments.of(argument1, argument2, "argument3"),
            4)
    );
}

JUnit Extension

Удобная штука, когда нужно выполнять одинаковый код до/после каждого/всех тестов. Чтобы не копипастить одно и то же, можно просто написать расширение и прикрепить его к базовому классу тестов, от которого наследуются все остальные. (Подробнее в документе JUnit.)

@ExtendWith(Extension.class)
public class BaseTest {
}

Финтифлюшки

Хочу поделиться еще одной полезной практикой в рамках управления ОТ на QA-стенде через SSH. Не буду показывать, как писать SSH-клиент, чтобы не забивать текст всем чем можно. Я собрал свой на базе mwiede/jsch, вы можете написать свой. Хочу заострить внимание на другом.

Задача — нужно менять лицензии на стенде, чтобы тестировать разный функционал платформы. По минимуму, список шагов, если делать через мягкие ссылки: удалить старую ссылку -> сделать новую -> перезапустить стенд — и умножить на 2, ибо в моем кейсе ОТ N+1. Можно сделать 6 команд через SSH, а можно сделать 2, предварительно написав баш-скрипт и положив его на стенды.

Пример:

#!/bin/bash

# Проверка аргументов
if [ $# -eq 0 ]; then
    echo "Ошибка: не указан тип лицензии" >&2
    exit 1
fi

LIC_TYPE="$1"
LICENSE_DIR="/etc/app"
LICENSE_FILE="${LICENSE_DIR}/licence.lic"
SOURCE_FILE="${LICENSE_DIR}/licence/licence.lic.${LIC_TYPE}"

# Проверка прав
if [ ! -w "$LICENSE_DIR" ]; then
    echo "Ошибка: недостаточно прав для записи в $LICENSE_DIR" >&2
    exit 1
fi

# Проверка существования исходного файла
if [ ! -f "$SOURCE_FILE" ]; then
    echo "Ошибка: файл лицензии не найден: $SOURCE_FILE" >&2
    exit 1
fi

# Установка лицензии
rm -f "$LICENSE_FILE"
if ln -s "$SOURCE_FILE" "$LICENSE_FILE"; then
    echo "Выбран тип лицензии: $LIC_TYPE"
    echo "Установлена лицензия: $LIC_TYPE"
    sudo systemctl restart app
    echo "App запущен с новой лицензией"
    echo "success"
else
    echo "Ошибка при создании символьной ссылки" >&2
    exit 1
fi

Смысл простой: предварительно складываем все лицензии в отдельную директорию, далее при вызове скрипта указываем тип лицензии, который нам нужен, происходят необходимые проверки, прокидывается символьная ссылка. Magic — и хороший повод для QA покачаться в админке.

Итоги

Что получилось? Добротный инструмент для шатания SOAP API со всех сторон, а именно:

  1. 2.7 тысячи тестов, на поддержку которых я потратил, от силы, часов 8-10 за год.
  2. Все красиво в Allure.
  3. Различные генераторы данных (не стал расписывать, ибо штука очень индивидуальная. Берегись пестицида!!!)
  4. SSH- и SFTP-клиент.
  5. Сборщики XML разного уровня сложности (есть еще XJC, но там не особо интересно).

Затраты

По затратам, я потратил на эту часть проекта полгода :) По 2–3 часа в день, если верить YouTrack, ушло 180 часов, которые мой работодатель оплатил (+ несколько часов Андрюхи на помощь мне, пару часов разраба). В это же время входит заведение кучи багов, обнаруженных на стадии проектировки всех API-запросов.

Мой кейс сугубо индивидуален из-за возни с SOAP. Если у вас RESTful-приложение, данная стадия займет у вас сильно меньше времени. Буквально собрать spec для Rest Assured и накидывать запросы через Map.of().

Профит

Немного ретроспективы, с чего все началось. Однажды я попал на регресс ручками. Регресс ОТ по тест-плану занял у меня… 5 недель. Добавлю, что 5 недель в жутком мыле, на фулл-тайме. Большая часть тест-кейсов довольно однотипная, и делать ее руками — смерти подобно. После этого случая появилось четкое желание автоматизировать хотя бы часть этого мероприятия.

По моим оценкам, этот MVP сократил возможный регресс на 3.5–4 недели — большую часть однотипных кейсов. Выходит, что 4 недели мы уложили в 30 минут прогона тестов. Stonks! Не мало важно, что такой частичный регресс можно прогонять хоть после каждой сборки. Итого мы сэкономили 4 недели, умноженные на количество запусков АТ. Неплохо!

Стоит дополнить, что все действия окупились еще на стадии проектировки запросов, так как нашлось множество мелких багов и недочетов в SOAP API, который никогда никто плотно не трогал.

По моему, все цели мы выполнили и даже перевыполнили! Успех! Приглашаю вас ознакомиться со 2-й частью.