Сколько стоит фреймворк автоматизации #1

Всем привет! Я Максим, 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-й частью.

Комментарии