DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Low-Code Development: Leverage low and no code to streamline your workflow so that you can focus on higher priorities.

DZone Security Research: Tell us your top security strategies in 2024, influence our research, and enter for a chance to win $!

Launch your software development career: Dive head first into the SDLC and learn how to build high-quality software and teams.

Open Source Migration Practices and Patterns: Explore key traits of migrating open-source software and its impact on software development.

Related

  • Keep Your Application Secrets Secret
  • Leveraging Test Containers With Docker for Efficient Unit Testing
  • Developing and Scaling a Microservice
  • Telemetry Pipelines Workshop: Installing Fluent Bit in Container

Trending

  • DZone's Article Submission Guidelines
  • Applying the Pareto Principle To Learn a New Programming Language
  • Spring AI: How To Write GenAI Applications With Java
  • Leveraging Microsoft Graph API for Unified Data Access and Insights
  1. DZone
  2. Software Design and Architecture
  3. Containers
  4. Fast Setup: Golang and Testcontainers

Fast Setup: Golang and Testcontainers

Most of the time, our scripts set up an entire test environment right on our computers, but why? In the test environment, we have everything, but we need it?

By 
Ilia Ivankin user avatar
Ilia Ivankin
·
May. 28, 24 · Tutorial
Like (1)
Save
Tweet
Share
1.3K Views

Join the DZone community and get the full member experience.

Join For Free

In this article, I want to discuss test containers and Golang, how to integrate them into a project, and why it is necessary.

Testcontainers Review

Testcontainers is a tool that enables developers to utilize Docker containers during testing, providing isolation and maintaining an environment that closely resembles production.

Why do we need to use it? Some points:

Importance of Writing Tests

  • Ensures code quality by identifying and preventing errors.
  • Facilitates safer code refactoring.
  • Acts as documentation for code functionality.

Introduction to Testcontainers

  • Library for managing Docker containers within tests.
  • Particularly useful when applications interact with external services.
  • Simplifies the creation of isolated testing environments.

Support Testcontainers-go in Golang

  • Port of the Testcontainers library for Golang.
  • Enables the creation and management of Docker containers directly from tests.
  • Streamlines integration testing by providing isolated and reproducible environments.
  • Ensures test isolation, preventing external factors from influencing results.
  • Simplifies setup and teardown of containers for testing.
  • Supports various container types, including databases, caches, and message brokers.

Integration Testing

  • Offers isolated environments for integration testing.
  • Convenient methods for starting, stopping, and obtaining container information.
  • Facilitates seamless integration of Docker containers into the Golang testing process.

So, the key point to highlight is that we don't preconfigure the environment outside of the code; instead, we create an isolated environment from the code. This allows us to achieve isolation for both individual and all tests simultaneously. For example, we can set up a single MongoDB for all tests and work with it within integration tests. However, if we need to add Redis for a specific test, we can do so through the code.

Let’s explore its application through an example of a portfolio management service developed in Go.

Service Description

The service is a REST API designed for portfolio management. It utilizes MongoDB for data storage and Redis for caching queries. This ensures fast data access and reduces the load on the primary storage.

Technologies

  • Go: The programming language used to develop the service.
  • MongoDB: Document-oriented database employed for storing portfolio data.
  • Docker and Docker Compose: Used for containerization and local deployment of the service and its dependencies.
  • Testcontainers-go: Library for integration testing using Docker containers in Go tests.

Testing Using Testcontainers

Test containers allow integration testing of the service under conditions closely resembling a real environment, using Docker containers for dependencies. Let’s provide an example of a function to launch a MongoDB container in tests:

Go
 
func RunMongo(ctx context.Context, t *testing.T, cfg config.Config) testcontainers.Container {
 mongodbContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
  ContainerRequest: testcontainers.ContainerRequest{
   Image:        mongoImage,
   ExposedPorts: []string{listener},
   WaitingFor:   wait.ForListeningPort(mongoPort),
   Env: map[string]string{
    "MONGO_INITDB_ROOT_USERNAME": cfg.Database.Username,
    "MONGO_INITDB_ROOT_PASSWORD": cfg.Database.Password,
   },
  },
  Started: true,
 })
 if err != nil {
  t.Fatalf("failed to start container: %s", err)
 }

 return mongodbContainer
}


And a part of the example:

Go
 
package main_test

import (
 "context"
 "testing"

 "github.com/testcontainers/testcontainers-go"
 "github.com/testcontainers/testcontainers-go/wait"
)

func TestMongoIntegration(t *testing.T) {
 ctx := context.Background()

 // Replace cfg with your actual configuration
 cfg := config.Config{
  Database: struct {
   Username   string
   Password   string
   Collection string
  }{
   Username:   "root",
   Password:   "example",
   Collection: "test_collection",
  },
 }

 // Launching the MongoDB container
 mongoContainer := RunMongo(ctx, t, cfg)
 defer mongoContainer.Terminate(ctx)

 // Here you can add code for initializing MongoDB, for example, creating a client to interact with the database

 // Here you can run tests using the started MongoDB container
 // ...

 // Example test that checks if MongoDB is available
 if err := checkMongoAvailability(mongoContainer, t); err != nil {
  t.Fatalf("MongoDB is not available: %s", err)
 }

 // Here you can add other tests in your scenario
 // ...
}

// Function to check the availability of MongoDB
func checkMongoAvailability(container testcontainers.Container, t *testing.T) error {
 host, err := container.Host(ctx)
 if err != nil {
  return err
 }

 port, err := container.MappedPort(ctx, "27017")
 if err != nil {
  return err
 }

 // Here you can use host and port to create a client and check the availability of MongoDB
 // For example, attempt to connect to MongoDB and execute a simple query

 return nil
}


How to run tests:

go test ./… -v


This test will use Testcontainers to launch a MongoDB container and then conduct integration tests using the started container. Replace `checkMongoAvailability` with the tests you need. Please ensure that you have the necessary dependencies installed before using this example, including the `testcontainers-go` library and other libraries used in your code.

Now, it is necessary to relocate the operation of the MongoDB Testcontainer into the primary test method. This adjustment allows for the execution of the Testcontainer a single time.

Go
 
var mongoAddress string

func TestMain(m *testing.M) {
    ctx := context.Background()
    cfg := CreateCfg(database, collectionName)

    mongodbContainer, err := RunMongo(ctx, cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        if err := mongodbContainer.Terminate(ctx); err != nil {
            log.Fatalf("failed to terminate container: %s", err)
        }
    }()

    mappedPort, err := mongodbContainer.MappedPort(ctx, "27017")
    mongoAddress = "mongodb://localhost:" + mappedPort.Port()

    os.Exit(m.Run())
}


And now, our test should be:

Go
 
func TestFindByID(t *testing.T) {
    ctx := context.Background()
    cfg := CreateCfg(database, collectionName)

    cfg.Database.Address = mongoAddress

    client := GetClient(ctx, t, cfg)
    defer client.Disconnect(ctx)

    collection := client.Database(database).Collection(collectionName)

    testPortfolio := pm.Portfolio{
        Name:    "John Doe",
        Details: "Software Developer",
    }
    insertResult, err := collection.InsertOne(ctx, testPortfolio)
    if err != nil {
        t.Fatal(err)
    }

    savedObjectID, ok := insertResult.InsertedID.(primitive.ObjectID)
    if !ok {
        log.Fatal("InsertedID is not an ObjectID")
    }

    service, err := NewMongoPortfolioService(cfg)
    if err != nil {
        t.Fatal(err)
    }

    foundPortfolio, err := service.FindByID(ctx, savedObjectID.Hex())
    if err != nil {
        t.Fatal(err)
    }

    assert.Equal(t, testPortfolio.Name, foundPortfolio.Name)
    assert.Equal(t, testPortfolio.Details, foundPortfolio.Details)
}


Ok, but Do We Already Have Everything Inside the Makefile?

Let's figure it out—what advantages do test containers offer now? Long before, we used to write tests and describe the environment in a makefile, where scripts were used to set up the environment. Essentially, it was the same Docker compose and the same environment setup, but we did it in one place and for everyone at once. Does it make sense for us to migrate to test containers?

Let's conduct a brief comparison between these two approaches.

Isolation and Autonomy

Testcontainers ensure the isolation of the testing environment during tests. Each test launches its container, guaranteeing that changes made by one test won’t affect others.

Ease of Configuration and Management

Testcontainers simplifies configuring and managing containers. You don’t need to write complex Makefile scripts for deploying databases; instead, you can use the straightforward Testcontainers API within your tests.

Automation and Integration With Test Suites

Utilizing Testcontainers enables the automation of container startup and shutdown within the testing process. This easily integrates into test scenarios and frameworks.

Quick Test Environment Setup

Launching containers through Testcontainers is swift, expediting the test environment preparation process. There’s no need to wait for containers to be ready, as is the case when using a Makefile.

Enhanced Test Reliability

Starting a container in a test brings the testing environment closer to reality. This reduces the likelihood of false positives and increases test reliability.

In conclusion, incorporating Testcontainers into tests streamlines the testing process, making it more reliable and manageable. It also facilitates using a broader spectrum of technologies and data stores.

Conclusion

In conclusion, it's worth mentioning that delaying transitions from old approaches to newer and simpler ones is not advisable. Often, this leads to the accumulation of significant complexity and requires ongoing maintenance. Most of the time, our scripts set up an entire test environment right on our computers, but why? In the test environment, we have everything — Kafka, Redis, and Istio with Prometheus. Do we need all of this just to run a couple of integration tests for the database? The answer is obviously no.

The main idea of such tests is complete isolation from external factors and writing them as close to the subject domain and integrations as possible. As practice shows, these tests fit well into CI/CD under the profile or stage named e2e, allowing them to be run in isolation wherever you have Docker!

Ultimately, if you have a less powerful laptop or prefer running everything in runners or on your company's resources, this case is for you!

Thank you for your time, and I wish you the best of luck! I hope the article proves helpful!

Code

DrSequence/testcontainer-contest

Read More

Testcontainers

MongoDB Module

MongoDB

Golang MongoDB Docker (software) Testing Container

Published at DZone with permission of Ilia Ivankin. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Keep Your Application Secrets Secret
  • Leveraging Test Containers With Docker for Efficient Unit Testing
  • Developing and Scaling a Microservice
  • Telemetry Pipelines Workshop: Installing Fluent Bit in Container

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: