Boosting your Tests with Elasticsearch using TestContainers-Go

Thursday, Sep 30, 2021 | 3 minute read

Ricardo Ferreira

TestContainers is a popular framework that allows Java developers to create tests that depend on backend systems such as Elasticsearch. It automatically creates a container instance for the said backend, preventing developers from manually spinning up their dependency instances. Also, it enables them to treat those instances as disposable, meaning that once the tests finish executing, the underlying containers created for the instances are destroyed automatically.

The good news for Go developers is that there is a version of this for them as well. It is called TestContainers-Go. In this post, I will walk you through creating a simple test that connects with Elasticsearch using this framework.

1. Install de TestContainers-Go dependency

go get github.com/testcontainers/testcontainers-go

2. Create a test code named elasticsearch_test.go

import (
	"testing"
)

func TestWithElasticsearch(t *testing.T) {
  // All the code is shown below comes here...
}

3. Create a container request for Elasticsearch

elasticsearchContainerRequest := testcontainers.ContainerRequest{
	Image:        "docker.elastic.co/elasticsearch/elasticsearch:7.15.0",
	Name:         "elasticsearch",
	ExposedPorts: []string{"9200/tcp"},
	Env: map[string]string{
		"node.name":             "single-node",
		"bootstrap.memory_lock": "true",
		"cluster.name":          "testcontainers-go",
		"discovery.type":        "single-node",
		"ES_JAVA_OPTS":          "-Xms1g -Xmx1g",
	},
	WaitingFor: wait.ForLog("Cluster health status changed from [YELLOW] to [GREEN]"),
}

Note that we provided a way for the code to wait until the container is ready for business. This is important to ensure that your code won’t start issuing requests to the container if it is not ready. The TestContainers-Go project provides different ways for you to check this. In this example, we used the wait.ForLog() function that monitors when the container log outputs the string provided as a parameter.

4. Create the actual container and start it

elasticsearchContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
	ContainerRequest: elasticsearchContainerRequest,
	Started:          true,
})
if err != nil {
	t.Errorf("Error creating the container: %s", err)
}
defer elasticsearchContainer.Terminate(ctx)

Note that we set the parameter Started to true to ensure that we want the container to be started automatically. Alternatively, you can set this to false and start the container programmatically. It may come in handy if you, for example, want to have fine control over when the container is effectively started.

5. Execute a request against Elasticsearch

endpoint, err := elasticsearchContainer.Endpoint(ctx, "")
if err != nil {
	t.Errorf("Error getting the Elasticsearch endpoint: %s", err)
}

response, err := http.Get(fmt.Sprintf("http://%s", endpoint))
if err != nil {
	t.Errorf("Error invoking the Elasticsearch endpoint: %s", err)
}

6. Use the response to verify correctness

type responseMock struct {
	ClusterName string `json:"cluster_name"`
}

body, err := ioutil.ReadAll(response.Body)
if err != nil {
	t.Errorf("Error deserialiizing the body: %s", err)
}

var actualResponse responseMock
json.Unmarshal(body, &actualResponse)

expectedResponse := responseMock{
	ClusterName: "testcontainers-go",
}

if actualResponse.ClusterName != expectedResponse.ClusterName {
	t.Errorf("Test failed. Cluster name was '%s' instead of '%s'",
		actualResponse.ClusterName, expectedResponse.ClusterName)
}

t.Log(string(body))

7. Finally, run the test in the terminal

go test -v

You should see an output similar to this:

=== RUN   TestWithElasticsearch
2021/09/30 13:48:12 Starting container id: 5845f4655900 image: quay.io/testcontainers/ryuk:0.2.3
2021/09/30 13:48:12 Waiting for container id 5845f4655900 image: quay.io/testcontainers/ryuk:0.2.3
2021/09/30 13:48:13 Container is ready id: 5845f4655900 image: quay.io/testcontainers/ryuk:0.2.3
2021/09/30 13:48:13 Starting container id: 01d37e098d0f image: docker.elastic.co/elasticsearch/elasticsearch:7.15.0
2021/09/30 13:48:13 Waiting for container id 01d37e098d0f image: docker.elastic.co/elasticsearch/elasticsearch:7.15.0
2021/09/30 13:48:25 Container is ready id: 01d37e098d0f image: docker.elastic.co/elasticsearch/elasticsearch:7.15.0
    elasticsearch_test.go:55: {
          "name" : "single-node",
          "cluster_name" : "testcontainers-go",
          "cluster_uuid" : "MjmXMAV6QhyBB-fP_JrRZg",
          "version" : {
            "number" : "7.15.0",
            "build_flavor" : "default",
            "build_type" : "docker",
            "build_hash" : "79d65f6e357953a5b3cbcc5e2c7c21073d89aa29",
            "build_date" : "2021-09-16T03:05:29.143308416Z",
            "build_snapshot" : false,
            "lucene_version" : "8.9.0",
            "minimum_wire_compatibility_version" : "6.8.0",
            "minimum_index_compatibility_version" : "6.0.0-beta1"
          },
          "tagline" : "You Know, for Search"
        }

--- PASS: TestWithElasticsearch (13.19s)
PASS
ok      testcontainers-go-elasticsearch 13.506s

Alternatively, you can (actually, should) use the official client for Go to interact with Elasticsearch without having to handle low-level HTTP requests:

newClient, err := elasticsearch.NewClient(elasticsearch.Config{
	Addresses: []string{
		fmt.Sprintf("http://%s", endpoint),
	},
})
if err != nil {
	panic(err)
}

Once the test finishes executing, you will notice that all containers started for this test will be destroyed automatically 😎

© 2018 - 2026 Ricardo Ferreira

Search is powered by Pagefind. Just hit CTRL+K or CMD+K to start searching.

Powered by Hugo with Dream and Devrel themes.

Open Source

I contribute to LangChain4j, an idiomatic open source Java library for building LLM-powered applications on the JVM. Recent work includes adding native vector search embedding stores so developers can build RAG, recommendation engines, and AI memory systems.

I also ported RedisVL to Go, an open source, AI-native client that brings vector search, semantic caching, LLM memory, semantic routing, rerankers, and an MCP server to the Redis ecosystem for Golang developers.

Public Speaking

I’ve been speaking at conferences since 2008 and doing it full time as part of my work with DevRel since 2018. My talks go deep on the systems I build with: distributed systems and event streaming, AI engineering and vector search, and the data infrastructure that has to hold up when the demo ends and production begins. Some of the events I’ve spoken at include AWS re:Invent, Microsoft Ignite, Google Cloud Next, KubeCon, Oracle OpenWorld, QCon, Strange Loop, Kafka Summit, Pulsar Summit, JavaOne, DevNexus, JFokus, JNation, and All Things Open.

Ricardo Ferreira presenting on the main stage at AI DevWorld
On the main stage at AI DevWorld

You can find my upcoming and past talks on my speaking calendar. Recordings also live on my YouTube channel, and the code I write for talks, demos, and workshops is on my GitHub.

Consulting and Professional Services

If you’d like to hire me as a consultant for your projects, speak at your event, or lead a hands-on workshop for your team, contact me at riferrei@riferrei.com. I can understand the scope of your request and provide a free estimate.

Who am I?

I work at the intersection of AI, data infrastructure, and distributed systems, turning complex technology into things developers can understand and products users love.

Lately, that means hands-on AI engineering: building vector search, semantic caching, agent memory, and RAG into the data layer, and figuring out how to make AI agents secure enough to ship. I contribute to open-source projects like LangChain4j and RedisVL for Golang.

The AI-native work isn’t a pivot. It draws on the same systems-design foundation I’ve built for 20+ years: designing data systems for scale, moving data fast, watching where systems break; now applied to vectors and agents. I have worked on RDBMS and Big Data at Oracle; event streaming with Apache Kafka and Apache Flink at Confluent; observability at Elastic; AI and developer tooling at AWS; and NoSQL and vector stores at Redis. That foundation is exactly what separates AI demos that work on stage from AI systems that survive production.

Social Links