Чтобы показывать вам рекламу — мы в GetIntent должны быть уверены в устойчивой и безотказной работе нашей рекламной платформы. Надежность системы складывается из многих компонентов: тип используемого железа, системная/сетевая конфигурация и архитектура приложения. Внесение изменений в достаточно сложные, распределенные приложения всегда несет в себе риск.
Разработчики со своей стороны пытаются минимизировать эти риски и пишут тесты: unit и интеграционные. Написание unit тестов обычно не составляет каких-либо трудностей. С интеграционными тестами, в зависимости от их изощренности, ситуация сложнее.



Когда тесты используют Tomcat или Jetty это не доставляет никаких проблем: эти сервера написаны на java и могут легко встраиваться в тесты. Но, например, мы используем Aerospike и когда мы хотим протестировать взаимодействие с этой базой данных, нас ожидают следующие трудности:

  • Aerospike написан не на Java и не может быть легко встроен в наше приложение.
  • Хочется чтобы разработчик мог запустить тесты на всех популярных платформах: Windows, OS X или Linux. Aerospike же предоставляет бинарники только для Linux.
  • Тесты могут выполняться параллельно, следовательно нам нужно несколько серверов?
  • Каждый тест должен получать в распоряжение чистый экземпляр базы данных.

Часто для этих целей используется общее тестовое окружение, например удаленный сервер, на котором уже настроен Aerospike и на котором можно запустить некоторые тесты. Однако, подход обладает определенными недостатками:

  • Запуск тестов на удаленном сервере занимает существенно больше времени, чем запуск локальных тестов. Это особенно заметно при работе с медленным интернетом.
  • Возникают проблемы с изоляцией, в случае, если несколько разработчиков решили одновременно протестировать приложение.
  • Довольно часто политика информационной безопасности компании требует прятать сервера за корпоративным VPN. Перспектива настройки VPN клиента может отвадить все желание работать из дома.

Альтернативный вариант: требовать подготовить локальное окружение для тестов, писать длинные инструкции по установке и настройке Aerospike для трех платформ (Win/Mac/Linux). Но есть и другой вариант — воспользоваться утилитами автоматизации, такими, как Docker.

Docker — это система развертывания и управления приложениями в изолированной среде (контейнерах). Он построен на принципах клиент-серверной архитектуры, docker client есть для всех основных OC, а docker daemon работает только на Linux — системах. Однако это не проблема: с помощью docker-machine можно запустить docker и на Windows/OS X (правда на виртуальном хосте). Итак, чтобы запустить тесты разработчику необходимо иметь на машине настроенный docker client — проверить это можно командой: docker run hello-world. Для Windows и OS X потребуется установить docker-machine.

Embedded aerospike


Для использования Aerospike в интеграционных тестах, мы написали обертку для docker и docker-machine. Она умеет:

  1. Стартовать / останавливать контейнеры.
  2. Смонтировать конфигурационный файл aerospike.conf внутрь контейнера.
  3. Привязать порт контейнера к свободному порту хоста.
  4. Стартовать / останавливать Docker Machine если тесты запускаются на Windows/OS X.

Для управления контейнерами мы используем docker-java API client - популярный Java API клиент для Docker. Чтобы запустить сервер Aerospike внутри Docker контейнера, настроить проброс портов и смонтировать конфигурационный файл, нужно выполнить команду:

docker run -d -P -p 3000:3000 -v
path/to/aerospike.conf:/etc/aerospike/aerospike.conf --name aerospike aerospike

А вот код, который делает тоже самое, используя Docker Remote Api


    ExposedPort tcp3000 = ExposedPort.tcp(3000);
    Volume volume = new Volume("/etc/aerospike/aerospike.conf");
    Ports portBindings = new Ports();
    portBindings.bind(tcp3000, Ports.binding(aerospikePort));
    CreateContainerResponse container = dockerClient.createContainerCmd(IMAGE_ID)
          .withExposedPorts(tcp3000)
          .withPortBindings(portBindings)
          .withBinds(new Bind(aerospikeConfPath, volume, AccessMode.ro))
          .exec();

    dockerClient.startContainerCmd(container.getId())
          .exec();

Более детально можно посмотреть в классе AerospikeServer.

Пишем тест


Давайте рассмотрим пример интеграционного теста с использованием embedded-aerospike. Допустим у нас есть класс SimpleAerospikeClient, который умеет хранить и получать сегменты пользователя по идентификатору.

    public Set getSegments(Long userId);
    public void addSegments(Long userId, Set segments);

Посмотреть полный класс.

Пишем тест, который проверяет корректность реализации данных методов.

Сначала надо настроить и запустить сервер.

    @BeforeMethod
    public void setUp() throws Exception {
        aerospikeServer = AerospikeServer.builder()
                .aerospikeConfPath(getClass().getResource("/aerospike.conf").getFile())
                .dockerConfig(DockerClientConfig.createDefaultConfigBuilder().build())
                .build();
        aerospikeServer.start();
    }

Проверяем, что данные правильно записываются и читаются из базы данных.

      @Test
      public void test() {
        long userId = ThreadLocalRandom.current().nextLong();
        aerospikeClient.addSegments(userId, new HashSet<Integer>() {{
            add(150);
            add(151);
        }});
        Set<Integer> segments = aerospikeClient.getSegments(userId);
        Assert.assertEquals(segments.size(), 2);
        Assert.assertTrue(segments.contains(150));
        Assert.assertTrue(segments.contains(151));
    }

Не забываем остановить и удалить контейнеры, которые были созданы во время тестов.

    @AfterMethod
    public void tearDown() throws Exception {
        aerospikeServer.stop();
    }

Заключение


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

Авторы статьи — kiruxan и zeliboba69
Поделиться с друзьями
-->

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


  1. methodx
    26.05.2016 17:49

    Статья отличная, но картинка — тяжелейшая форма депрессии.


    1. hanovruslan
      26.05.2016 19:17
      +3

      Это из иллюстрации к корнею чуковскому. Для вас это бред, а для нас (постарше) это тест на школоло )) шутка, без обид.

      А если серьезно то тк автору у меня вопросы
      непонятно зачем так реализовывать модуль к фреймворку тестирования (у вас гольный junit или как?)

      Стал ли этот вспомогательный код частью приложения?

      Не похоже ли это на антипаттерн тестирование моков?


      1. zeliboba69
        27.05.2016 13:08

        hanovruslan, под вспомогательным кодом вы имеете ввиду SimpleAerospikeClient.java? Это, скажем так, упрощенная версия того, что используется в приложении.

        Не совсем понял про тестирование моков, ведь идея как раз в том, чтобы написать интеграционный тест. Тест, который, в данном случае, будет проверять взаимодействие приложения с базой данных: как минимум, правильно ли вы настроили клиент для работы с базой, правильно ли выбрали структуру данных для хранения, и т.д.


        1. hanovruslan
          27.05.2016 13:36

          zeliboba69
          Да, про тестирование моков я немного не в тему сказал. Задам другой вопрос ) не знаю как там в джава, но в php есть варианты проще перехватывать подобные обращения (у вас их к слову нет, вы поднимаете какой-то инстанс нужного сервиса) — SoftMocks или runkit. это инструменты которые перегружают библиотечные (недоступные в исходном виде) модули\функции, чтобы обеспечить как и в вашем случае — интеграционные тесты. Так вот привлечение докера (только для интеграционных тестов?) так как-бы немного сбоку смотрится странным решением.

          Может быть следует сделать сборку на docker-compose для нескольких окружений (dev, test, prod) где не потребуется использовать SimpleAerospikeClient а взаимодействие клиентов и их серверами будет обеспечено сетью, которую поднимает докер при запуске группы контейнеров? Это, скорее всего сложнее, но выглядит имхо канонично в рамках использования контейнеров

          а еще что у вас с фреймворком тестирования? Это JUnit? А есть ему альтернативы?


          1. olegchir
            29.05.2016 19:21

            альтернатива JUnit, например, TestNG. Но зачем ему альтернативы? JUnit просто как автомат калашникова, там с десяток методов с общим смыслом assertTrue(message, desired, actual) и всё. Код JUnit'а простейший, можно допиливать его и допиливать как хочешь. Вроде, там просто нечего заменять, это и так минимальный минимум.


            1. Borz
              29.05.2016 21:32

              преимущество TestNG перед JUnit как минимум в более удобных DataProvider. В остальном — всё равно, что TestNG, что JUnit. Ну, разве что только то, что в интернете больше примеров по JUnit, чем по TestNG


  1. jrip
    26.05.2016 19:49

    >Для использования Aerospike в интеграционных тестах, мы написали обертку для docker и docker-machine. Она умеет:
    >Стартовать / останавливать контейнеры.
    >Смонтировать конфигурационный файл aerospike.conf внутрь контейнера.
    >Привязать порт контейнера к свободному порту хоста.

    А почему не использовали для этого Chef?


  1. bsideup
    26.05.2016 23:10

    Как один из мэйнтейнеров проекта не могу не спросить:

    А почему не http://github.com/testcontainers/testcontainers-java? :)


    1. bsideup
      26.05.2016 23:14

      Просто мы в ZeroTurnaround тестируем наши java agent-ы с множеством разных баз данных и фреймворков, сначала написали свой test engine поверх docker-java, потом появился TestContainers и мы решили объединить силы :)


      1. kiruxan
        27.05.2016 12:45

        см, коммент ниже:)


  1. kiruxan
    27.05.2016 12:44

    Все просто: хоть статья и написана недавно, но данный подход мы используем с 2015 года когда проект TestContainers видимо только стартовал, ну и плюс все тесты в проекте на TestNG. Но библиотека у вас очень интересная, как минимум смотреть в её сторону имеет смысл!


    1. bsideup
      27.05.2016 12:48

      https://github.com/testcontainers/testcontainers-java/issues/132 :)
      На самом деле у нас тоже были тесты на TestNG, мы просто создавали контейнеры из TC и вручную вызывали .start() и .stop().

      Как и написал выше, мы тоже начали со своего, а потом просто присоединились к TestContainers. OpenSource FTW! :)

      Можете постучаться в разного рода IM-ы (Gitter, Skype ), я помогу смигрировать на TC если надо :)