Статьи

Быстрое и простое интеграционное тестирование с Docker и Overcast

 

[Эта статья была написана Полом ван дер Энде.]

Проблемы с интеграционным тестированием

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

  • Mongo не написан на Java, поэтому мы не можем легко внедрить его в наше Java-приложение
  • Нам нужно где-то установить и настроить MongoDB, поддерживать установку или писать сценарии, чтобы настроить ее как часть нашего тестового прогона.
  • Каждый тест, который мы запускаем на сервере Монго, будет изменять состояние, и тесты могут влиять друг на друга. Мы хотим максимально изолировать наши тесты.
  • Мы хотим протестировать наш драйвер на нескольких версиях MongoDB.
  • Мы хотим провести тесты как можно быстрее. Если мы хотим запустить тесты параллельно, нам нужно несколько серверов. Как мы ими управляем?

Давайте попробуем решить эти проблемы.

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

Пасмурно и докер

логотипМы собираемся использовать Docker и Overcast . Возможно, вы уже знаете Docker. Это технология для запуска приложений внутри программных контейнеров. Overcast — это библиотека, которую мы будем использовать для управления докером. Overcast — это Java-библиотека с открытым исходным кодом,
разработанная XebiaLabs, чтобы помочь вам написать тест для подключения к облачным хостам. В Overcast есть поддержка различных облачных платформ, включая EC2 , VirtualBox , Vagrant , Libvirt (KVM). Недавно я добавил поддержку Docker в Overcast версии 2.4.0 .

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

Мы будем использовать Overcast для создания контейнеров Docker, работающих на сервере MongoDB. Overcast поможет нам получить динамически доступный порт от хоста Docker. Хост в нашем случае всегда будет хостом докера. Docker в нашем случае работает на внешнем хосте Linux. Overcast будет использовать TCP-соединение для связи с Docker. Мы сопоставляем внутренние порты с портом на хосте докера, чтобы сделать его доступным извне. MongoDB будет внутренне работать на порту 27017, но Docker отобразит этот порт на локальный порт в диапазоне от 49153 до 65535 ( определяется docker ).

Настройка наших тестов

Давайте начнем. Во-первых, нам нужен образ Docker с установленным MongoDB. Благодаря сообществу Docker, это так же просто, как повторно использовать один из уже существующих образов из Docker Hub . Вся тяжелая работа по созданию такого образа уже сделана для нас , и благодаря контейнерам мы можем запустить его на любом хосте, способном запускать док-контейнеры. Как настроить Overcast для запуска контейнера MongoDB? Это наша минимальная конфигурация, которую мы помещаем в файл с именем overcast.conf:

mongodb {
    dockerHost="http://localhost:2375"
    dockerImage="mongo:2.7"
    exposeAllPorts=true
    remove=true
    command=["mongod", "--smallfiles"]
}

Это все! DockerHost настроен на локальный хост с портом по умолчанию. Это значение по умолчанию, и вы можете его пропустить. Образ докера с именем mongo version 2.7 будет автоматически извлечен из центрального реестра докеров. Мы устанавливаем для exposeAllPorts значение true, чтобы сообщить докеру, что ему необходимо динамически отобразить все открытые порты с помощью образа докера. Мы устанавливаем remove в true, чтобы убедиться, что контейнер автоматически удаляется при остановке. Обратите внимание, что мы перезаписываем команду запуска контейнера по умолчанию, передавая дополнительный параметр «—smallfiles» для повышения производительности тестирования. Для нашей настройки это все, что нам нужно, но в overcast также есть поддержка определения статических сопоставлений портов, установки переменных среды и т. Д. Более подробную информацию можно найти в документации по Overcast .

How do we use this overcast host in our test code? Let’s have a look at the test code that sets up the Overcast host and instantiates the mongodb client that is used by every test. The code uses the TestNG @BeforeMethod and @AfterMethod annotations.

private CloudHost itestHost;
private Mongo mongoClient;
 
@BeforeMethod
public void before() throws UnknownHostException {
    itestHost = CloudHostFactory.getCloudHost("mongodb");
    itestHost.setup();
 
    String host = itestHost.getHostName();
    int port = itestHost.getPort(27017);
 
    MongoClientOptions options = MongoClientOptions.builder()
        .connectTimeout(300 * 1000)
        .build();
 
    mongoClient = new MongoClient(new ServerAddress(host, port), options);
    logger.info("Mongo connection: " + mongoClient.toString());
}
 
@AfterMethod
public void after(){
    mongoClient.close();
    itestHost.teardown();
}

It is important to understand that the mongoClient is the object under test. Like mentioned before, we borrowed this library to demonstrate how one would integration test such a library. The itestHost is the Overcast CloudHost. In before(), we instantiate the cloud host by using the CloudHostFactory. The setup() will pull the required images from the docker registry, create a docker container, and start this container. We get the host and port from the itestHost and use them to build our mongo client. Notice that we put a high connection timeout on the connection options, to make sure the mongodb server is started in time. Especially the first run it can take some time to pull images. You can of course always pull the images beforehand. In the @AfterMethod, we simply close the connection with mongoDB and tear down the docker container.

Writing a test

The before and after are executed for every test, so we will get a completely clean mongodb server for every test, running on a different port. This completely isolates our test cases so that no tests can influence each other. You are free to choose your own testing strategy, sharing a cloud host by multiple tests is also possible. Lets have a look at one of the tests we wrote for mongo client:

@Test
public void shouldCountDocuments() throws DockerException, InterruptedException, UnknownHostException {
 
    DB db = mongoClient.getDB("mydb");
    DBCollection coll = db.getCollection("testCollection");
    BasicDBObject doc = new BasicDBObject("name", "MongoDB");
 
    for (int i=0; i < 100; i++) {
        WriteResult writeResult = coll.insert(new BasicDBObject("i", i));
        logger.info("writing document " + writeResult);
    }
 
    int count = (int) coll.getCount();
    assertThat(count, equalTo(100));
}

Even without knowledge of MongoDB this test should not be that hard to understand. It creates a database, a new collection and inserts 100 documents in the database. Finally the test asserts if the getCount method returns the correct amount of documents in the collection. Many more aspects of the mongodb client can be tested in additional tests in this way. In our example setup, we have implemented two more tests to demonstrate this. Our example project contains 3 tests. When you run the 3 example tests sequentially (assuming the mongo docker image has been pulled), you will see that it takes only a few seconds to run them all. This is extremely fast.

Testing against multiple MongoDB versions

We also want to run all our integration tests against different versions of the mongoDB server to ensure there are no regressions. Overcast allows you to define multiple configurations. Lets add configuration for two more versions of MongoDB:

defaultConfig {
    dockerHost="http://localhost:2375"
    exposeAllPorts=true
    remove=true
    command=["mongod", "--smallfiles"]
}
 
mongodb27=${defaultConfig}
mongodb27.dockerImage="mongo:2.7"
 
mongodb26=${defaultConfig}
mongodb26.dockerImage="mongo:2.6"
 
mongodb24=${defaultConfig}
mongodb24.dockerImage="mongo:2.4"

The default configuration contains the configuration we have already seen. The other three configurations extend from the defaultConfig, and define a specific mongoDB image version. Lets also change our test code a little bit to make the overcast configuration we use in the test setup depend on a parameter:

@Parameters("overcastConfig")
@BeforeMethod
public void before(String overcastConfig) throws UnknownHostException {
    itestHost = CloudHostFactory.getCloudHost(overcastConfig);

Here we used the paramaterized tests feature from TestNG. We can now define a TestNG suite to define our test cases and how to pass in the different overcast configurations. Lets have a look at our TestNG suite definition:

<suite name="MongoSuite" verbose="1">
    <test name="MongoDB27tests">
        <parameter name="overcastConfig" value="mongodb27"/>
        <classes>
            <class name="mongo.MongoTest" />
        </classes>
    </test>
    <test name="MongoDB26tests">
        <parameter name="overcastConfig" value="mongodb26"/>
        <classes>
            <class name="mongo.MongoTest" />
        </classes>
    </test>
    <test name="MongoDB24tests">
        <parameter name="overcastConfig" value="mongodb24"/>
        <classes>
            <class name="mongo.MongoTest" />
        </classes>
    </test>
</suite>

With this test suite definition we define 3 test cases that will pass a different overcast configuration to the tests. The overcast configuration plus the TestNG configuration enables us to externally configure against which mongodb versions we want to run our test cases.

Parallel test execution

Until this point, all tests will be executed sequentially. Due to the dynamic nature of cloud hosts and docker, nothing limits us to run multiple containers at once. Lets change the TestNG configuration a little bit to enable parallel testing:

<suite name="MongoSuite" verbose="1" parallel="tests" thread-count="3">

This configuration will cause all 3 test cases from our test suite definition to run in parallel (in other words our 3 overcast configurations with different MongoDB versions). Lets run the tests now from IntelliJ and see if all tests will pass:

Снимок экрана 2014-10-08 в 8.32.38 вечера
We see 9 executed test, because we have 3 tests and 3 configurations. All 9 tests have passed. The total execution time turned out to be under 9 seconds. That’s pretty impressive!

During test execution we can see docker starting up multiple containers (see next screenshot). As expected it shows 3 containers with a different image version running simultaneously. It also shows the dynamic port mappings in the «PORTS» column:

Screen Shot 2014-10-08 at 8.50.07 PM

That’s it!

Summary

To summarise, the advantages of using Docker with Overcast for integration testing are:

  1. Minimal setup. Only a docker capable host is required to run the tests.
  2. Save time. Minimal amount of configuration and infrastructure setup required to run the integration tests thanks to the docker community.
  3. Isolation. All test can run in their isolated environment so the tests will not affect each other.
  4. Flexibility. Use multiple overcast configuration and parameterized tests for testing against multiple versions.
  5. Speed. The docker container starts up very quickly, and overcast and testng allow you to even parallelize the testing by running multiple containers at once.

The example code for our integration test project is available here. You can use Boot2Docker to setup a docker host on Mac or Windows.

Happy testing!