Dec 10th, 2023: [EN] Ensuring compatibility with two Elasticsearch versions (or more) in automated tests

Upgrades of key dependencies can be tedious and scary. What if your system won’t work, because we haven’t tested one of the key flows? To avoid that we test our systems in staging, but that takes time and effort. Result? We postpone that for long, sometimes too long.

Luckily for us there is a remedy. Instead of running the tests manually, we can have the checks automated with real dependencies. In this entry we will use Testcontainers with Java (but you’re free to use other supported languages too).

In case you haven't used Testcontainers so far, it's a family of libraries in a few languages (like Java, Go, Python, .NET etc.) and what it does for you, is setting up and controlling Docker containers (running whatever you need) just from your test suite, using the programming language you know. So no YAML is needed, no docker run or Docker compose. To start containers with your actual dependencies, you just use your programming language. And the library takes care of stopping them automatically after your tests complete.

The only requirement by Testcontainers is that Docker-compatible environment has to be present in the place where you want to run them.

What's also very useful, is that compared to mastering command lines or YAML files to set things up correctly, you can rely on so called Testcontainers modules. A module in this case knows how to configure the thing running inside it correctly, what config flags should be set, which ports should be exported and so on, so you don't have to do that manually. For this example we're going to use Elasticsearch Testcontainers module.

We can use Testcontainers to run many containers at the same time. So why shouldn’t we run automated tests, relying on two Elasticsearch versions, the one we already have and the one we want to switch to, during the very same run?
This way we can rest assured that the tested use cases work with both versions in a single run, and (perhaps more importantly), we can ease upgrades in the future by simply changing the image versions.

public class TwoContainersTest {

   static final ElasticsearchContainer es7Container =
           new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:7.17.15")
                   .withPassword(ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD);
   static final ElasticsearchContainer es8Container =
           new ElasticsearchContainer("docker.elastic.co/elasticsearch/elasticsearch:8.11.1");

   static ElasticsearchClient es7Client;
   static ElasticsearchClient es8Client;

   @BeforeAll
   static void setUpContainersAndClients() {

       Startables.deepStart(es7Container, es8Container).join();

       CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
       credentialsProvider.setCredentials(
               AuthScope.ANY,
               new UsernamePasswordCredentials("elastic", ElasticsearchContainer.ELASTICSEARCH_DEFAULT_PASSWORD)
       );
       JacksonJsonpMapper jsonpMapper = new JacksonJsonpMapper();

       RestClient restClient7 = RestClient
               .builder(HttpHost.create(es7Container.getHttpHostAddress()))
               .setHttpClientConfigCallback(httpClientBuilder ->
                       httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
               ).build();

       es7Client = new ElasticsearchClient(new RestClientTransport(restClient7, jsonpMapper));

       RestClient restClient8 = RestClient
               .builder(new HttpHost(es8Container.getHost(), es8Container.getMappedPort(9200), "https"))
               .setHttpClientConfigCallback(httpClientBuilder ->
                       httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider)
                               .setSSLContext(es8Container.createSslContextFromCa())
               ).build();

       es8Client = new ElasticsearchClient(new RestClientTransport(restClient8, jsonpMapper));
   }

   @Test
   void whatIsItForTest() throws IOException {
       for (ElasticsearchClient esClient : Arrays.asList(es7Client, es8Client)) {
           InfoResponse info = esClient.info();
           Assertions.assertEquals("You Know, for Search", info.tagline());
       }
   }
}

Of course, instead of a test case calling info(), we shall have tests covering our actual cases.

A few hints:

  • withPassword allows us to change the default password if we’re not happy with it in the tests
  • Startables.deepStart(...) starts all the containers in parallel, so we can save time which would be wasted if we started them one by one. (In case you’re relying on the @Testcontainers annotation, you can use @Testcontainers(parallel=true) on static fields).
  • The RestClient for version 8 sets the SSL Context, so we don’t have to disable security (which is enabled by default since this version).
  • In our solution we never expose the ports as fixed ports, because:
    • it’s impossible to have two containers to use the same fixed port in a single run,
    • what’s more, when not using random port (with getMappedPort(9200)), you’re tempted to hardcode the port in your production code, which might be incorrect in actual production deployment.
  • We shall not use "localhost" instead of container.getHost(), because while it works on your machine, it might not always work in your CI.

If you'd like to check the full example, with dependencies, it's available on GitHub.

Happy Upgrades using two boxes!

1 Like

This topic was automatically closed 28 days after the last reply. New replies are no longer allowed.