Сколько стоит фреймворк автоматизации #1
Всем привет! Я Максим, QA, автоматизатор, TestOps’тизатор, жнец, хлебец, на дуде игрец. У меня есть довольно интересный опыт построения автоматизации с нуля, и я решил им поделиться. Попытаюсь изложить историческую канву, личный опыт, описать, какие инструменты внедрял и почему, сколько это стоило в часах и каких результатов достиг.
читать первым в телеграм читать первым в макс
Дисклеймер
Так уж сложилось, что я джавушник, поэтому большая часть кода будет представлена для Java, се-ля-ви. Какой-то код будет применим чисто к Java, какой-то будет использовать популярные решения, написанные на разных ЯП. Основная цель — показать, какие задачи стояли, какие мысли были, какие решения использовали и какие шишки набиты. Идейно можно взять любой код и перенести его на ваш любимый ЯП.
Статья разделена на две части. В первой больше упор на разное кунг-фу с Java. Вторая часть больше про докер и БД.
Словарик
Некоторые термины могут отличаться в рамках ЯП. Во избежание недопонимания ниже небольшой словарик:
- Смаршалить/Анмаршалить — иначе говоря, сериализовать/десериализовать Java-объект в строку/файл/XML/JSON и обратно.
- POJO (Plain Old Java Object) — простой Java-объект, не зависящий от специфических фреймворков и не расширяющий заранее заданные классы. Обычно используется для упаковки и переноса данных.
- АТ — платформа автотестов, которая крутит тесты и которую я разрабатываю.
- ОТ — объект тестирования, или тестируемое приложение.
Вводные
Что дано? 10-летний легаси Java-зверь с тонной функционала (в дальнейшем — ОТ), а именно:
- SOAP API, которое за 10 лет никто толком не шатал.
- REST API для работы с web.
- Лицензии, которые меняют функционал платформы.
- Разные конфиги, которые тоже меняют функционал платформы.
- SNMP, SFTP, JMX-функционал.
- Платформа активно работает с сетевыми протоколами, что равно дампы-дампы-дампы.
Ситуация не сахар, а еще и ушел Димка, но что же есть на старте? Коллекция в 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-раннер все берет из гитовой репы, а не с вашего личного ПК).
<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, подкладывание собранного тела в запрос и т.д. Тут скажу лишь: каждый дрочит как хочет делает как считает нужным.
Определяем требования
- ТК ОТ на Java, в компании все жабущники, значит, АТ пилим тоже на Java. Продать идею Python-проекта сложно в таких обстоятельствах (да и не хочется).
- На стадии MVP ожидается около 700 тестов, это около 1.5 тысяч SOAP-запросов на ОТ.
- XML для SOAP-запросов должны собираться из кода. Никаких захардкоженных файлов. Со 130 было тяжело, с 1.5 тысяч будет хуже…
- Allure-отчеты, обязательно.
- Нужно будет прикрутить всякие SNMP, SFTP, SSH, JMX-клиенты.
- Различные генераторы данных. Мы чо, не автоматизаторы????
- Возможность собирать XML не только для SOAP, но и как документ для загрузки на ОТ.
- Ну и прочие финтифлюшки по мелочи.
MVP
Самым сложным на стадии MVP был 3-й пункт требований. SOAP в полуживом состоянии, все инструменты и документация от Apache, в основном. Готовых решений и мануалов — кот наплакал. После сношений с гуглом, ИИ, StackOverflow я и Бадди (в дальнейшем Андрюха) находим его — CXF.
Фреймворк для работы с SOAP, есть все нужные либы — JAX-WS, JAXB, JAX и прочие, но что для нас было важно?
- Умеет генерировать POJO из WSDL-проекта.
- Собирает SOAP-клиент и отправляет запросы.
- Генерирует объектную фабрику (JAXB) и прочие штуки.
Что это дает?
- Не надо писать вручную кучу-прекучу POJO и поддерживать их.
- Этот же фреймворк используется в ОТ. Совместимость 100%.
Чуть подробнее об этом я писал тут.
Здесь же опишу те проблемы, с которыми столкнулся при использовании вышеописанного функционала:
- Боль моя — дырка задница при работе с Apache-документацией.
- 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, его нужно смаршалить. Вот несколько решений:
- Самое дешевое — подключить Jackson в Java (или любой другой аналог в вашем ЯП). Действует просто, не требует жесткого фикса типа объекта, XML генерит тоже простые:
<object>
<param1>...<param1>
...
</object>
Если вам этого достаточно, смело пользуйте. Доку найдете в интернетах.
- Чуть более 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>
И генерирует из них два класса:
public class YouService extends jakarta.xml.ws.Service, который хранит адреса, пути, инициализацию Service.- Сам 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. У этого решения есть свои плюсы:
- Логирование из коробки.
- Allure из коробки.
- Общепринятый HTTP-клиент, бест-практис, имеет множество альтернатив на разных ЯП.
- Один клиент и на REST, и на SOAP.
Минусы:
- Придется посношаться с хедерами и прочим.
- Нужно придумать, как добавлять тело запроса в
<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 со всех сторон, а именно:
- 2.7 тысячи тестов, на поддержку которых я потратил, от силы, часов 8-10 за год.
- Все красиво в Allure.
- Различные генераторы данных (не стал расписывать, ибо штука очень индивидуальная. Берегись пестицида!!!)
- SSH- и SFTP-клиент.
- Сборщики 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-й частью.
