Testcontainers for Containerized Integration Testing at Moogsoft
Nithya Janarthanan | June 25, 2020

In this blog post, we’ll share how Testcontainers made it so much easier to write integration and component tests here at Moogsoft.

In this blog post, we’ll share how Testcontainers made it so much easier to write integration and component tests here at Moogsoft.

Introduction

Here at Moogsoft, we take quality seriously and one of the most important goals for our test suites is to catch issues early on in the development process. A lot of our automated tests are integrated into our CI/CD (Continuous Integration/Continuous Deployment) pipeline as gates that can block a merge request with quality issues. Therefore, to ensure stable CI/CD pipelines as well as quick and quality releases to production, it is important to have tests that are stable and lightweight. So in this blog, we are going to see how we used Testcontainers to achieve some of the aforementioned goals.

What is Testcontainers

Before going into the why, let’s quickly see what is Testcontainers. Quoting the Testcontainers site directly: “Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container.”

Testcontainers wraps Docker in a Java API with JUnit/TestNG integration. As mentioned above, Testcontainers supports many commonplace dependencies, such as databases (MongoDB, MySQL), messaging queues (Kafka), and search engines (Elastic), to name a few. It is popular in Java, but has support for Python, Golang, and others. The full list is here.

Why Testcontainers

Now let’s look at some of our reasons for using Testcontainers at Moogsoft. Below we highlight some of the issues we had, and explain how we addressed them by using Testcontainers in our automation framework.

Life without Testcontainers

We heavily depended on a combination of unit tests, API tests and end-to-end (E2E) tests for verification of product quality. (We also have test automation for non-functional testing such as performance, scale, and resiliency, but that’s not within the scope of this blog post). Unit tests with mock dependencies were painful to maintain by refactoring, and they are not the same as testing against an actual deployment of the product. The E2E tests, on the other hand, tested things in an actual deployment, but they were often time-consuming and flaky, resulting in a long-running pipeline, which as we all know is not good for continuous deployment. This is where Testcontainers came to our rescue.

Life with Testcontainers

We quickly realized that Testcontainers is powerful, letting us write tests at the component level and integration level, while staying away from mocks but allowing us to spin up actual containers of the dependencies, such as databases, message queues, search engines or other micro services. Since we could write a lot of our tests at the component level or integration level, we are now able to run a lot of the tests very early in the development lifecycle, thereby catching our bugs earlier, instead of waiting for the E2E tests that run at the end. Also, our E2E tests have shrunk drastically, resulting in a pipeline that is much faster and stable.

What is the test scenario?

We needed to write tests for a TestService that had dependencies on Kafka, MySQL, and MongoDB, as well as on a Flyway upgrade. TestService itself was responsible for consuming messages from a Kafka topic, processing the message and updating MySQL tables. Let’s first look at the container-creation logic below.

This is the code snippet to create a Kafka container:

public class ExampleTest {
@BeforeClass(alwaysRun = true)
public void setup() throws Exception {
   kafka = new KafkaContainer()
       .withCreateContainerCmdModifier(cmd -> cmd.withName("kafka"))
     	 .withExternalZookeeper("zookeeper:2181")
     	 .withExposedPorts(9092, 9093)
     	 .withNetworkAliases("kafka-svc");

    zookeeper = new GenericContainer("confluentinc/cp-zookeeper:4.0.0")
       .withNetwork(kafka.getNetwork())
       .withNetworkAliases("zookeeper")
  	 .withEnv("ZOOKEEPER_CLIENT_PORT", "2181")
}

Here’s the code snippet to create a MySQL container with a database initialized. withEnv("MYSQL_DATABASE","sampleDB") allows the creation of an empty database that is needed by Flyway to load the database schema:

mysql = new GenericContainer<>("mysql:latest")
         .withCreateContainerCmdModifier(cmd -> cmd.withName("mysql"))
         .withEnv("MYSQL_DATABASE","sampleDB") 
         .withExposedPorts(3306)
         .withEnv("MYSQL_ROOT_PASSWORD", "root")
         .withNetwork(kafka.getNetwork());

Now that we have created a MySQL container with an empty database named SampleDB, let’s look at how to load database schema with tables that will later be updated by the service:

sharedSQLDB = new GenericContainer<>("sharedsqldb:latest")
      .withCreateContainerCmdModifier(cmd -> cmd.withName("sharedsqldb"))
    	.withNetwork(kafka.getNetwork())
.withCopyFileToContainer(MountableFile.forHostPath(flywayConf.getAbsolutePath()), "/flyway/conf/flyway.conf")
     	.withCommand("migrate")
     	.withStartupTimeout(Duration.ofSeconds(30));

Finally, we want to start the TestService microservice that needs to be tested. For application specific services that are not supported by Testcontainers, you can create a custom container using a generic container by providing an image:

testService = new GenericContainer<>("test_service:latest")
       .withEnv("KAFKA_BOOTSTRAP_SERVERS","kafka-svc:9092")
       .withEnv("DB_HOST", "mysql")
       .withEnv(DB_SCHEMA","sampleDB")
       .withEnv("DB_USER","root")
       .withEnv(DB_PASSWORD","root")
       .withExposedPorts(3030)
       .withNetwork(kafka.getNetwork());

Networking

By default, each container gets an isolated network. If our containers need to communicate with each other, we can enlist them in a Testcontainer network to establish communication between two or more containers as follows:

  • Create the first container in a network and add the rest of the containers to the same network using   .withNetwork(kafka.getNetwork());
testService = new GenericContainer<>("test_service:latest")
       .withEnv("KAFKA_BOOTSTRAP_SERVERS","kafka-svc:9092")
       .withEnv("DB_HOST", "mysql")
       .withEnv(DB_SCHEMA","sampleDB")
       .withEnv("DB_USER","root")
       .withEnv(DB_PASSWORD","root")
       .withExposedPorts(3030)
       .withNetwork(kafka.getNetwork());

Complete Test Code

public class TestServiceTest {
 
   private static final String MY_TOPIC = "testTopic.v1";
   private static int EW_PORT = 3030;
   private KafkaContainer kafka;
   private GenericContainer testService;
   private GenericContainer sharedsql;
   private GenericContainer testService;
   private GenericContainer zookeeper;
   private GenericContainer mysql;
   private int mappedPort;
 
   @BeforeClass(alwaysRun = true)
   public void setup() throws Exception {
 
       File flywayConf = new File("src/test/resources/flyway.conf");
//Kafka container 
       kafka = new KafkaContainer()
               .withCreateContainerCmdModifier(cmd -> cmd.withName("kafka"))
               .withExternalZookeeper("zookeeper:2181")
               .withExposedPorts(9092, 9093)
               .withNetworkAliases("kafka-svc");
// zookeeper container 
       zookeeper = new GenericContainer<>("zookeeper:3.6.1")
               .withCreateContainerCmdModifier(cmd -> cmd.withName("zookeeper"))
               .withNetworkAliases("zookeeper")
               .withEnv("ZOOKEEPER_CLIENT_PORT", "2181")
               .withExposedPorts(2181);
 
//Mysql  container 
       mysql = new GenericContainer<>("mysql:5.7")
               .withCreateContainerCmdModifier(cmd -> cmd.withName("mysql"))
               .withExposedPorts(3306)
               .withEnv("MYSQL_DATABASE", "sampleDB")
               .withEnv("MYSQL_ROOT_PASSWORD","root")
               .withNetwork(kafka.getNetwork());
 
// SharedSQLDB container 
       sharedsql = containerProvider
               .provide(EContainerType.GENERIC, yamlProps, Optional.of(CCommonConstants.SHARED_SQL_DB))
               .withNetwork(kafka.getNetwork())
               .withCopyFileToContainer(MountableFile.forHostPath(flywayConf.getAbsolutePath()), "/flyway/conf/flyway.conf")
               .withCommand("migrate")
               .withStartupTimeout(Duration.ofSeconds(30))
               .withNetwork(kafka.getNetwork());
 
//TestService  container 
       testService = new GenericContainer<>("test_service:latest")
               .withEnv("MOOG_KAFKA_BOOTSTRAP_SERVERS","kafka-svc:9092")
               .withEnv("MOOG_DB_HOST", "mysql")
               .withEnv("MOOG_DB_SCHEMA","sampleDB")
               .withEnv("MOOG_DB_USER","root")
               .withEnv("MOOG_DB_PASSWORD","root")
               .withExposedPorts(3030)
               .withNetwork(kafka.getNetwork());
 
	kafka.start()
zookeeper.start()
mysql.start()
sharedsql.start()
testService.start()
          }
 
   @Test(testName = "Basic")
   public void assert_posted_msg_appears_on_kafka_bus() throws SQLException {
       String testPayload = CExpressUtil.readFile("src/test/resources/testPayload.json");
 
       CKafkaProducer producer = new CKafkaProducer(kafka);
       producer.send(MY_TOPIC, testPayload);
 
       // Read alerts from kafka. Topic name is .test.v1
       CKafkaConsumer kafkaConsumer = new CKafkaConsumer(kafka, Collections.singletonList(MY_TOPIC));
       ConsumerRecords records = kafkaConsumer.getMessages();
       assertTrue(records.size() > 0, “No records found in Kafka topic”);
   }
 
   @AfterClass(alwaysRun = true)
   public void teardown() {
	zookeeper.stop();
	kafka.stop();
	mysql.stop();
	sharedsql.stop();
	testService.stop();
   }
}

Conclusion

I could keep going on and on about multiple reasons why Testcontainers is powerful, but I’d like to conclude by highlighting some of our key takeaways:

  • Setting up the infrastructure needed for testing was easy and inexpensive. Those of you who have slogged through writing mock objects and custom setup steps for integration tests will understand why.
  • It simplified writing and maintaining tests.
  • It eased integration with CI, since most CI tools support running containers.

Thanks to everyone who took the time to read this article. I hope it was helpful to anyone trying to figure out how to write simple, lightweight and yet powerful integration tests. Please feel free to reach out to us with any questions or feedback.

Moogsoft is a pioneer and leading provider of AIOps solutions that help IT teams work faster and smarter. With patented AI analyzing billions of events daily across the world’s most complex IT environments, the Moogsoft AIOps Platform helps the world’s top enterprises avoid outages, automate service assurance, and accelerate digital transformation initiatives.
See Related Posts by Topic:

About the author

mm

Nithya Janarthanan

Nithya Janarthanan is an Engineering Lead at Moogsoft. She has around 12 years of experience in software and quality assurance, specializing in the creation and design of automation frameworks and in the delivery of tools to develop better quality software. She also has a strong commitment to innovation and a deep interest in emerging trends in software development and testing. When she is not coding or busy breaking code, she loves painting and playing badminton.

All Posts by Nithya Janarthanan

Moogsoft Resources

June 25, 2020

Testcontainers for Containerized Integration Testing at Moogsoft

June 8, 2020

Continuous Integration & Delivery @ Moogsoft: GitLab and Jenkins Integration

June 8, 2020

Hello World! Moogsoft Engineers Start Blogging

Loading...