О чём статья?

Если вам необходимо автоматизировать тестирование веб-сокетов, то эта статья будет для вас полезна. В ней я поделюсь своим опытом "прикручивания" библиотеки Spring Boot Starter Websocket к проекту автотестов на Java.

По данной теме в интернете можно найти много гайдов, но во всех них приводятся примеры клиентов на JavaScript. Поэтому мне показалось полезным собрать в одном месте необходимые шаги для создания клиента на Java. 

Эта статья - своего рода инструкция для быстрого запуска WebSocket-клиента в вашем проекте. Поэтому я намеренно опущу описание ряда деталей (например, что такое веб-сокеты и STOMP-протокол) и подробное описание интерфейсов библиотеки. Вы сможете узнать подробнее о терминах в других источниках, таких как эта статья, и обратиться к документации пакета.

Предисловие

Пара слов о тестируемом мной продукте: это клиентское web-приложение, которое взаимодействует с бэком через http- и ws-протоколы. 

Немного о самих автотестах: проект на Java, используется сборщик Maven, тестовый фреймворк TestNG + чистый Selenium, шаблон проектирования тестов PageObject.

Для контроля качества продукта в тестах недостаточно пройти какие-либо пользовательские сценарии от точки А до точки В. Требуется также проверить, что отображаемые на фронте данные соответствуют бизнес-логике и полученным сообщениям от сервера. Для этой цели в автотестах для работы с API обычно используют RestAssured, но данная библиотека не позволяет работать с веб-сокетами. Поэтому мы обращаемся за помощью к Spring Boot Starter WebSocket. 

Почему Spring Boot Starter Websocket, ведь есть иные библиотеки для работы с веб-сокетами на Java (например, Java WebSocket)?
1. В интернете по данной библиотеке можно найти больше информации: как минимум, есть документация.
2. В Java WebSocket есть ряд уязвимостей, связанных с зависимостями (например, уязвимость Denial of Service). 

Рис. 1 Уязвимости Java WebSocket
Рис. 1 Уязвимости Java WebSocket

Итак, с помощью библиотеки Spring Boot Starter Websocket в проекте автотестов мы создадим клиента. Он будет взаимодействовать с веб-сокетом и выполнять следующие функции:

  1. Подключение к WebSocket (кстати, под капотом библиотеки реализован handshake, поэтому нам не нужно отдельно продумывать update http-запроса до веб-сокета).

  2. Подписка на топик, получение всех сообщений от сервера по веб-сокету.

  3. Отправка сообщений на сервер по открытому веб-сокету.

  4. Отписка от топика.

  5. Закрытие соединения по веб-сокету. 

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

Step by step...

1. Для создания клиента сначала добавляем библиотеку в проект. Указываем зависимость в pom:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.7.17</version>
</dependency>

2. Создаём два класса:
a) WSClient. В нём будут инициализироваться классы библиотеки, и будет вызываться метод подключения к веб-сокету. 
b) StompSessionHandler. Класс содержит описание действий клиента после подключения к веб-сокету. 

3. Вызываем метод firstWSClient в самих тестах. 

Далее привожу примеры кода для реализации клиента. 
Еще оставлю ссылку на примеры из документации.

WSClient

public class WSClient {
    public void firstWSClient() {

        // Инициализируем классы библиотеки
        WebSocketClient webSocketClient = new StandardWebSocketClient();
        WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
        stompClient.setMessageConverter(new MappingJackson2MessageConverter());
        StompSessionHandler myStompSessionHandler = new StompSessionHandler();

        // Определяем заголовки при обновлении http до ws (handshake)
        WebSocketHttpHeaders websocketheaders = new WebSocketHttpHeaders();
        // Если требуется передавать авторизационные данные (токен или пароль)
        websocketheaders.set("Content-Type", "text/css;charset=UTF-8");

        // Определяем заголовок сообщений, передаваемых по веб-сокету
        StompHeaders stompHeaders = new StompHeaders();
        // Например, указываем топик, на который хотим подписаться
        stompHeaders.set(StompHeaders.DESTINATION, "topic/message");

        // Указываем URL веб-сокета, к которому требуется подключиться
        String URL = "http://localhost:8080/";

        // Подключение к веб-сокету
        ListenableFuture<StompSession> sessionAsync = stompClient.connect(URL, websocketheaders, stompHeaders, myStompSessionHandler);
        StompSession stompSession = sessionAsync.get(5, TimeUnit.SECONDS);

        // Подписываемся на топик
        stompSession.subscribe(stompHeaders, myStompSessionHandler);

        // Оправка сообщения по веб-сокету на сервер
        stompSession.send(stompHeaders, "Hello, World!");
    }
}

Для работы клиента нужно определить и передать заголовки для http-запроса-«рукопожатия», чтобы открыть веб-сокет. Скорее всего, вам потребуется передать авторизационные данные, например, токен (строки 10-13).

Также определите топик, на который вам нужно подписаться. Это можно выяснить самому, посмотрев подключение через DevTools браузера на вашем фронте. Либо спросите своих backend-разработчиков.

Топик передаём в заголовке сообщения по веб-сокету, на этапе подписки. Само наполнение хедера отражено в строках 15-18.

Далее в примере происходит подключение, подписка и отправка сообщения (строки 23-31). Обращу внимание, что методы subscribe, send перегружены и могут использоваться с другими аргументами. Например, можно не использовать stompHeaders при вызове subscribe, а только передать топик строкой (destination). С методом send такая же картина. 

StompSessionHandler

public class StompSessionHandler extends StompSessionHandlerAdapter {

    // Описываем действия с подключением к веб-сокету
    // В этом примере выводим в консоль, что подключение есть
    @Override
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        System.out.println("Opening websocket...");
        System.out.println("Session is connected: " + session.isConnected() + "\n");
        }

// Обработчик сообщений из веб-сокета
    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
            String json = new String((byte[]) payload);
            System.out.println("Received json:\n" + json + "\n");
    }
}

В данном примере метод afterConnected выведет в консоль информацию об успешном подключении к веб-сокету. 

Метод handleFrame выводит в консоль сообщения из веб-сокета. С помощью него также можно организовать маппинг сообщений для тестов, где можно будет сравнить полученное по веб-сокету сообщение с данными, собранными через Selenium на фронте.

Инициализация клиента в тестах

@Test(description = "Получение данных по веб-сокету")
public void getWSTestData(){
        WSClient wsClient = new WSClient();
        wsClient.firstWSClient();
    }

Добавляем строки инициализации класса WSClient и вызов метода firstWSClient в нужных тестах, где требуется работа с веб-сокетом.

Заключение

Надеюсь, данная инструкция поможет сэкономить время при создании клиента по работе с WebSocket’ами в ваших проектах по автотестированию на Java. Пусть уровень качества тестируемых продуктов становится только выше!

Комментарии (3)


  1. sshikov
    11.11.2023 11:17
    +2

    2. В Java WebSocket есть ряд уязвимостей, связанных с зависимостями (например, уязвимость Denial of Service). 

    Хм. А какое вам дело до DDoS, если вы на этом собираетесь тесты писать? Это не означает, что надо было выбрать Java WebSocket, просто это странная мотивация при выборе. Более того, эти уязвимости - они все в определенных версиях, причем довольно старых. Ну так, к примеру:

    CVE-2022-45688 @ Maven-org.json:json-20131018 - как вам версия 2013 года (при наличии двух десятков более свежих?)


    1. valeryvasilyev Автор
      11.11.2023 11:17

      Справедливое замечание


      1. sshikov
        11.11.2023 11:17
        +2

        Кстати, если копнуть глубже, это эта уязвимость сводится к тому, что если на вход методу, преобразующему XML в json, подсунуть XML с открывающими тэгами очень глубокой вложенности, то будет, сюрприз, stack overflow. Ну т.е. не знаю как где, а в Java это исключение вполне себе можно отловить, и продолжить работу. Даже нужно отловить - потому что приложение, которое не ловит исключения при парсинге XML, ему и так дорога в помойку. То есть это в принципе и на DDoS не очень-то тянет.