Как приступить к автоматизации WebSocket API

Так, с REST API разобрались. Там есть ендпоинты, в которые можно швырять реквесты и получать ожидаемые респонсы, запихивая их в модели. А как же быть с WebSocket API, в основе которого лежит wss соединение. Клиент создает постоянное соединение с сервером и отправляет туда данные (тоже например в json формате) и получает ответы (необязательно), а также отправляет уведомления о том, что сообщение принято.

Ладно, как создать wss соединение с сервером? Подключим для начала через мавен библиотеку:

<dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp-ws</artifactId>
            <version>3.4.1</version>
</dependency>

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

public final class WebSocketClient implements WebSocketListener

В этом классе заимплементим метод, который запускает соединение:

private final ExecutorService writeExecutor = Executors.newSingleThreadExecutor();
    private WebSocket sharedConnection;
    private int msgNumber = 900;

    public void run(String token) throws IOException, InterruptedException {
        String wssUrl = ConfigurationInstance.getInstance().getWssUrl(); // Получаем адрес wss api через конфигурацию нашу
        publicToken = token;

        OkHttpClient client = new OkHttpClient.Builder()
                .readTimeout(0,  TimeUnit.MILLISECONDS)
                .build();

        Log.console(\"Connecting to websocket...\");

        Request request = new Request.Builder() // Добавляем актуальные хедеры для нашего апи
                .url(wssUrl)
                .addHeader(\"Authorization\", \"Bearer \" + publicToken)
                .addHeader(\"Sec-WebSocket-Protocol\", \"json\")
                .addHeader(\"User-Agent\", \"\")
                .addHeader(\"Accept-Language\", \"ru-RU\")
                .build();
        WebSocketCall.create(client, request).enqueue(this);

        // Trigger shutdown of the dispatcher's executor so this process can exit cleanly.
        client.dispatcher().executorService().shutdown();
    }

    @Override public void onOpen(final WebSocket webSocket, Response response) {
        writeExecutor.execute(new Runnable() {
            @Override public void run() {
                Log.console(\"Client '\" + publicToken + \"' ONLINE\");
                sharedConnection = webSocket;
            }
        });
    }

И файналли создаем метод который триггерится когда приходит сообщение с сервера. Здесь мы уже конструируем базовое общение с сервером. Например, если приходит сообщение, на которое нужно подтверждение о получении, то отправляем его. А если приходит сообщение ping, отвечаем pong, таким образом давая понять, что соединение не разорвано:

@Override public void onMessage(ResponseBody message) throws IOException {
        String wsServerMessage = message.string();
        Log.console(\"< RECEIVED MESSAGE: \" + wsServerMessage);

        JsonParser parser = new JsonParser();
        JsonElement element = parser.parse(wsServerMessage);
        JsonArray wsServerMessageArr = element.getAsJsonArray();

        String messageType = wsServerMessageArr.get(0).toString();
        String messageName = wsServerMessageArr.get(2).toString();
        String receivedMsgNumber = wsServerMessageArr.get(1).toString();

        if (Objects.equals(messageType, \"1\")) { // Сообщение типа 1 - нужно ответить что принял его

            JsonElement wsData = wsServerMessageArr.get(3);

            int answerType = 2;
            String ackMess = \"[\" + answerType + \",\" + msgNumber++ + \",\" + receivedMsgNumber + \"]\";
            Log.console(\"> SENDING MESSAGE: \" + ackMess);
            sharedConnection.sendMessage(RequestBody.create(TEXT, ackMess)); // Отправляем 

        } else if (Objects.equals(messageType, \"0\")) { // 0 Сообщение типа 0 - отвечать не надо
            if (Objects.equals(messageName, \"\\\"svc.ping\\\"\")) { // Но если это пинг, то пингануть в ответ
                int answerType = 0;
                String answerMessageName = \"svc.pong\";

                String sendMsg = \"[\" + answerType + \",\" + msgNumber++ + \",\\\"\" + answerMessageName + \"\\\"]\";
                Log.console(\"> SENDING MESSAGE: \" + sendMsg);
                sharedConnection.sendMessage(RequestBody.create(TEXT, sendMsg));
            }
        }
        message.close();
    }

Далее в этом же классе реализуем методы, которые будут отправлять определенные сообщения на сервер при получении от него каких-то данных. Тут уже всё зависит от вашего апи, какие активности нужно покрыть. Этого я уже показывать не буду, разобраться далее не сложно.

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

Для начала понимания материала хватит, дальше изи должно быть.

Тестируем REST API с Retrofit2

У меня полностью разработана система на Java, которая тестирует запросы в REST API, получая заранее подготовленные данные из json файлов. Здесь я покажу минимальные куски кода, которые помогут понять, как создать Java классы, которые будут содержать все ендпоинты вашего REST API. Для начала нужно установить Retrofit 2 и кажется он требует okhttp3:

<dependency>
            <groupId>com.squareup.retrofit2</groupId>
            <artifactId>retrofit</artifactId>
            <version>2.1.0</version>
</dependency>
<dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.4.1</version>
</dependency>

Создадим файл ApiService.java, которые будет содержать запросы в API:

public interface ApiService {
    @GET(\"api/user\")
    Call<JsonElement> getUser(@Header(\"Accept-Language\") String locale,
                              @Header(\"Authorization\") String token);
}

GET это тип запроса, в скобках путь к ендпоинту апи. Внутри — заголовки которые мы передаем.

Ещё мы создадим класс, который тоже будет содержать методы, каждый из которых будет вызывать методы из класса ApiService, но с индивидуальными обработчиками. Зачем это нужно? Ну, например, каждый из запросов порождает определенного формата ответ от сервера, и модели этих ответов мы можем создать заранее, и легко работать потом с данными из этих ответов прямо в тестах. Вот пример метода из класса с запросами ApiEndpoints.java:

public class ApiEndpoints {
  public static UserToken getUser(String token) throws IOException {
        String locale = ConfigurationInstance.getInstance().getLocale(); // получение значения из конфигурации
        String authHeaderValue = \"Bearer \" + token;
        Call<JsonElement> call = RestClient.getInstance().getApiService().getUser(locale, authHeaderValue);
        Response response = call.execute();
        Object responseObject = response.body();
        Gson gson = new Gson();
        String json = responseObject.toString();
        UserToken responseUserToken = gson.fromJson(json, UserToken.class);
        return responseUserToken;
    }
}

Тут ответ преобразуется в тип UserToken. Это модель ответа сервера на такой запрос и она создана заранее.

Ну и наконец вызов запроса из самого теста:

@Test(description = \"GET /api/user\", groups = {\"User\"})
    public void getUser(ITestContext context) throws IOException, InterruptedException {
        String token = (String) context.getAttribute(\"token\"); //токен берется из конфигураций или прошлых запросов
        UserToken userTokenResponse = ApiEndpoints.getUser(token); //вызов запроса
    }

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

Позже классы ApiService и ApiEndpoints разрастаются и содержат все ендпоинты вашего апи. В методы класса ApiEndpoints можно долбиться поочередно в рамках какого-то юзер-стори, ре-юзая данные из ответов одних запросов как входные данные для других запросов, и так далее. В общем простор для фантазий.

Сурсы в этом посте немного пообрезаны и могут не работать в чистом виде, но думаю разобраться что нужно добавить будет уже не сложно.

Нахрен CSV, давай парсить тест данные из json

CSV слишком простой и тупой для хранения данных тестов, предлагаю хранить эти данные в json, т.к. json огонь и его все любят.

1 Ставим json.simple через Maven:

<dependency>  
    <groupId>com.googlecode.json-simple</groupId>  
    <artifactId>json-simple</artifactId>  
    <version>1.1.1</version>  
</dependency>

2 Пишем метод, который принимает как сырые данные json формата, так и путь к .json файлу:

public static JSONObject parseJsonData(String data) throws ParseException, IOException {  
    boolean isJsonContent = data.substring(0, 1).matches(\"\\\\{\");  
    Object object;  
    JSONParser parser = new JSONParser();  
    if (isJsonContent) object = parser.parse(data);  
    else object = parser.parse(new FileReader(data));  
    return (JSONObject) object;  
}

3 Он возвращает обьект типа JSONObject, с которым можно ковыряться удобными способами:

Object jsonRequests = jsonObject.get(\"Requests\");  
  
int currentRequestId = 0;  
  
JSONObject request = (JSONObject) jsonRequests.get(currentRequestId);  
String requestName = (String) request.get(\"Name\");  
JSONArray testCases = (JSONArray) request.get(\"TestCases\");

Из JSONObject можно выковыривать любые данные, а с JSONArray работать как с обычным массивом.

Как через testng.xml передать параметр только одному методу?

На сайте нашел кучу любых методов передачи параметров из testng.xml:

<?xml version="1.0" encoding="UTF-8"?>  
<suite name="ParameterSuite">  
  <parameter name=\"suiteParam\" value=\"suiteScoped\"></parameter>  
  <parameter name=\"commonParam\" value=\"suiteValue\"></parameter>  
  <test name=\"MethodParamScopedTest\">  
    <parameter name=\"testParam\" value=\"testScoped\"></parameter>  
    <classes>  
      <parameter name=\"classesParam\" value=\"classesScoped\"></parameter>  
      <class name=\"com.javarticles.testng.TestClass1\">  
        <parameter name=\"classParam\" value=\"classScoped\"></parameter>  
        <parameter name=\"commonParam\" value=\"overridenClassValue\"></parameter>  
      </class>  
      <class name=\"com.javarticles.testng.TestClass2\"/>  
    </classes>  
  </test>  
</suite>

Но, что за подстава, самый глубокий из них передает параметры классу тестов, а как передать только одному методу? А вот как:

<test verbose=\"1\" name=\"addCard\" annotations=\"JDK\" preserve-order=\"true\">  
    <classes>  
        <class name=\"app.passenger.User\">  
            <methods>  
                <include name=\"changeUserInfo\"/>  
            </methods>  
        </class>  
        <class name=\"app.passenger.Payments\">  
            <methods>  
                <include name=\"addCard\"/>  
                <include name=\"addCard\">  
                    <parameter name=\"testCase\" value=\"Success2ndCard\"/>  
                </include>  
            </methods>  
        </class>  
    </classes>  
</test>

Запилил виджет JIRA для Dashing-JS

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


Дашинг отображает любые виджеты, запиленные на HTML и SCSS, а данные получает скриптом на CoffeeScript.
А чо такое Dashing-JS? Это порт дашинга на Node.js. Т.е. данные он получает скриптом на Node.js.
На Гитхабе разместил виджет, который получает из JIRA список задач по запросу JQL и отображает их на виджете.
https://github.com/vglushonkov/dashing-js-list-jira-issues

Как передать параметр из Jenkins -> ANT -> TestNG

  1. В Jenkins идем в настройки Джоба и выставляем \»This build is parameterized\»
  2. Называем параметр \»additionalScreens\» например
  3. В ant.xml проставляем
    <property name=\"additionalScreens\" value=\"\"/>
    
  4. В ant.xml в таргете не забываем передать этот параметр в TestNG:
    <target name=\"Test.Real\" depends=\"compile\">  
            <testng outputdir=\"${testdir}\" classpathref=\"all.classpath\" haltOnFailure=\"true\" testnames=\"Buy\">  
                <xmlfileset dir=\"${basedir}\" includes=\"${testngxml}\"/>  
                <jvmarg value=\"-DadditionalScreens=${additionalScreens}\" />  
            </testng>  
    </target>
    
  5. В TestNG.xml параметр никак не фигурирует, там просто вызывается тест:
    <test verbose=\"1\" name=\"Buy\" annotations=\"JDK\">  
        <classes>  
            <class name=\"TestNG.Buy\">  
                <methods>  
                    <include name=\"shop\"/>  
                    <include name=\"Scan\"/>  
                    <include name=\"Buy\"/>  
                </methods>  
            </class>  
        </classes>  
    </test>
    
  6. В методе @BeforeClass принимаем параметр и дальше работаем с ним как хотим:
    @Parameters({ \"additionalScreens\" })  
        @BeforeClass  
        public void prepare(String additionalScreens, ITestContext context) throws Exception {  
            context.setAttribute(\"additionalScreens\", additionalScreens);  
            //......  
    }
    
  7. Запускаем Джоб в Дженкинсе и заполняем или выбираем параметр \»additionalScreens\» — он передается в тест. Это может быть, например, урл сервера, или тип проверки, и т.д.
    Не благодари!

Как заставить выполняться @AfterClass в TestNG

Заметил, что метод @AfterClass не выполняется, если хоть один из тестов к классе завалился или пропущен. Так как заставить её выполняться, ведь там идет и driver.quit() и весь репортинг в стороннюю мониторинг систему?
Похоже на \»Как высыпаться за 30 минут в день — читайте в моей новой книге \»Никак, блядь\».
Лучше сделаем из этого метода такой:

@AfterGroups(\"thisClassGroup\")  
    public void report() throws Exception{  
        driver.quit();  
        if (Objects.equals(fullErrorMessage, \"\")) fullErrorMessage = \"OK\";  
        Log.report(fullErrorMessage, \"thisClassGroup\", finalErrorType);  
    }

А всем тест методам пропишем

@Test(description = \"Test1\", groups = \"thisClassGroup\")

Как проверить наличие элемента, не завалив тест

Запиши себе эту функцию, очень пригодится. Вызываем её, чтобы проверить наличие элемента, а потом, например, кликаем его. Отсутствие элемента не завалит тест. Передаем туда конструкцию By и драйвер.

elementExists(By.xpath(nestedCoupon), driver)
public static boolean elementExists(By by, WebDriver driver) {  
    try {  
        driver.findElement(by);  
    } catch (NoSuchElementException e) {  
        return false;  
    }  
    return true;  
}

Как получать данные из CSV для тестов на TestNG

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

@DataProvider(name = \"Data\")  
    public Iterator<Object []> dataReader() throws InterruptedException {  
        List<Object []> dataLines = new ArrayList<>();  
        boolean notDataHeader = false;  
        String[] data;  
        BufferedReader br;  
        String line;  
        String workingDirectory = System.getProperty(\"user.dir\");  
        String filePath = workingDirectory + \"\\\\TestNG\\\\data\\\\\" + dataFile;  
  
        try {  
            br = new BufferedReader(new FileReader(filePath));  
            while ((line = br.readLine()) != null) {  
                data = line.split(\";\");  
                if (notDataHeader) dataLines.add(data);  
                notDataHeader = true;  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return dataLines.iterator();  
    }

Как подсунуть DataProvider к тесту и так ясно:

@Test(description=\"Описание теста\", dataProvider = \"Data\")  
    public void pay(String uin, String price) throws Exception {  
       dotest();  
    }

Это .csv файл с двумя значениями, подходящий к данному примеру. Если нужно добавить значений, не забываем добавить столько же входных параметров для функции public void pay:

uin;price  
12;990

Как заставить TestNG и ANT разговаривать по русски

Вчера мучался с этим полдня, обгуглился до дрожи в пальцах, но так и не нашел решение. TestNG при запуске через IDEA и своих отчетах выдавал мне ??? вместо русских букв. Самый фак был ещё и в том, что в testng.xml присутствовали параметры на русском языке и ничего не работало. Докопался до того, что при запуске сборки Ant она сама запускает JVM со своими параметрами, такими как classpath и ea (enable assertions), но как туда впихнуть -Dfile.encoding=UTF-8 ?

Оказывается, в таргете внутри ant.xml нужно всего лишь указать следующее (см. строку 4):

<target name=\"test\" depends=\"compile\">  
    <testng outputdir=\"${testdir}\" classpathref=\"all.classpath\" haltOnFailure=\"true\">  
        <xmlfileset dir=\"${basedir}\" includes=\"testng.xml\"/>  
        <jvmarg value=\"-Dfile.encoding=UTF-8\" />  
    </testng>  
</target>