Tutorial on TestContainers with Cassandra

Kalpani Ranasinghe
6 min readOct 16, 2023
Photo by Ihor Malytskyi on Unsplash

Hello everyone! I’m here again with another interesting and timely topic. This time it is about Testing. As developers, we are always responsible for executing unit testing of the functionalities we implement. Performing different types of tests before merging your feature implementations can reduce many defects during deployment.

The unit tests aim to test the business logic implementations, isolating them from external services like databases, messaging systems, etc. But in reality, a considerable amount of code might still be bound with these services. Therefore, writing integration tests has become a necessity.

Implementation of Integration tests can be difficult because we need to make sure the infrastructure is ready and running and the relevant data is available, before test execution. Also if you have multiple build pipelines, test pollution can happen.

Although, we can use in-memory services to overcome the aforementioned issues, they may not have all the features of production service (i.e. query that works for Sqlite might not work for CQL). However, there is a solution available for this issue: Testcontainers.

What is TestContainers?

TestContainers is a testing library that has support for different languages and frameworks i.e. Java, Dotnet, Ruby, Go, Clojure, etc. Using Testcontainers, we can write tests talking to the same type of services you use in production without mocking or using in-memory services.

A typical Testcontainers-based test works as follows:

  • Start your required services (databases, messaging systems, etc.) in docker containers using Testcontainers API.
  • Establish connection between the containerized service and the application.
  • Run the tests using the new containers.
  • After the tests, Testcontainers will take care of destroying the containers irrespective of whether the tests are executed successfully or not.

TestContainers for .Net contains predefined modules for some popular databases like MSSql, CosmosDb, MongoDb, etc. and, some other services like RabbitMQ, Elasticsearch, and Redis. However, it still doesn’t contain a module for Cassandra. However, we can directly use the generic ContainerBuilder() to generate a container instance with predefined Cassandra data volumes.

Now you might be wondering what are these modules. Modules are implemented to provide default configuration settings for some services which can be overridden according to our requirements. Also, modules promote reusability and standardize container creation. TestContainers GitHub repository contains a template for creating a module. TestContainers for Java already contains a Cassandra module.

Example project on Testcontainers

Since now you know the basics, let’s dive into an example so you can understand the concepts better. The following link contains a simple application that uses TestContainers and I’m using it as an example for this tutorial. First things first. You need to have docker installed on your PC before starting.

This project contains two subprojects TestContainersSampleProject and TestContainerSampleProject.StudentService.Tests. The first one contains the StudentService. The next project is a xunit test project in which we include the test classes and methods. In this project, we install the Testcontainers for the Dotnet Nuget package.

Install the first package on the top

Also, we have a docker-compose file where we define the docker image, volumes, ports, etc. As the first step what we have to do is run this file. This will create a docker container using Cassandra’s latest docker image (which is in the docker hub) and start a local container immediately. You also need to specify the place where you need to store Cassandra data in the volumes section. This will be mapped to the /var/lib/cassandra in the container.

version: '3'
services:
cassandra:
image: new-image
container_name: new-container-student
ports:
- 9042:9042
volumes:
- local_directory_path:/var/lib/cassandra

Student class contains student ID, first name, last name, and age while StudentService contains functions to create, update, delete, and retrieve a single student by his student ID, and retrieve all the students.

Program.cs is responsible for creating a connection with the container and a CQL table to store student data.

using Cassandra;

namespace TestContainersSampleProject;

class Program
{
static void Main(string[] args)
{
// Initialize Cassandra cluster and session as mentioned earlier
// Define the Cassandra cluster
var cluster = Cluster.Builder()
.AddContactPoint("127.0.0.1") // Replace with your Cassandra cluster's contact point
.Build();

// Create a session
var session = cluster.Connect();

// Create the keyspace if it doesn't exist
session.Execute(@"
CREATE KEYSPACE IF NOT EXISTS StudentDetails
WITH replication = {
'class': 'SimpleStrategy',
'replication_factor': 1
}");

// Create students table
session.Execute(@"
CREATE TABLE IF NOT EXISTS StudentDetails.students (
student_id UUID PRIMARY KEY,
first_name TEXT,
last_name TEXT,
age INT
)");
}
}

Let’s run the project TestContainersSampleProject. Before running the application, make sure the Cassandra container that we created previously is running in the background. This will create a Cassandra cluster in the container and open up a connection. After the connection stabilizes, it creates a student table in the Cassandra database.

You can also create some APIs to perform the functionalities in the StudentService. Then you can add some data to the table. I haven’t created any APIs since the main aim is to discuss the Testcontainers and how it works.

StudentTests class contains a few tests but you can eventually add more. As you can see the container creation is different in the test project because it has used testcontainer’s generic container builder.

private readonly IContainer _container = new ContainerBuilder()
.WithImage("cassandra:latest")
.WithName("cassandra-testing-student")
.WithPortBinding(9042, false)
.WithResourceMapping(new DirectoryInfo(local_directory_path), "/var/lib/cassandra")
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(9042))
.Build();

I have used the Cassandra docker image as I mentioned earlier and you can change the Cassandra instance name by using .WithName() otherwise it will be a random name. You can also set the ports that the application connects to the container and map an existing device location to the Cassandra volumes. Also, we can define a WaitStrategy where the container will wait until the mentioned port is available.

private bool CreateConnection()
{
try
{
_cluster = Cluster.Builder()
.AddContactPoint(_container.Hostname)
.Build();

_session = _cluster.Connect();
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Exception occurred: {ex}");
return false;
}
}

Inside the CreateConnection() function it creates a cassandra cluster and connection to it. This creates a new session.

We can use IAsyncLifetime in Xunit to start the container and destroy it after the test execution. Xunit calls IAsyncLifetime.InitializeAsync() immediately after the class creation. Therefore inside that function, we can include _container.StartAsync() to start the container. IAsyncLifetime.DisposeAsync() calls after the test execution. In this function, we include _container.DisposeAsync().AsTask() which stops and deletes the container which was running.

Now I can show you what happens when we start our tests. For this, I’m going to choose the TestStudentCreation(). This test creates a student and adds the details to the students table in the studentdetails key space. Then retrieve the student by the generated Id and check whether the created student and the retrieved student are the same.

When you debug the test with breakpoints at the end of the function, you can see two containers getting started in the docker desktop.

Two containers get created.

After the breakpoint is reached, you can connect to the database through CLI. Then, execute a select command to check whether the data has been correctly added to the table.

Connect to the CQL database through the CLI

After execution of this single test, you can see the container gets destroyed automatically. You can execute other tests, implement new tests and, get more familiar with the process with Testcontainers.

That’s all about the basics of Testcontainers for dotnet. Hope you learned something new. Please let me know your thoughts, comments and, suggestions.

Thank you!

References

--

--

Kalpani Ranasinghe

Backend Developer | Graduate Student at University of Oulu