API

Как приступить к автоматизации 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 можно долбиться поочередно в рамках какого-то юзер-стори, ре-юзая данные из ответов одних запросов как входные данные для других запросов, и так далее. В общем простор для фантазий.

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