MySQL Docker Container For Integration Testing Using Go

Mitesh
ITNEXT
Published in
4 min readDec 27, 2018

--

Photo by Erwan Hesry on Unsplash

Bugs are most expensive when comes in production. Catching them during the development process using test cases is one of the best things we can do to lower our cost. Testing is very important in all software. This helps ensure the correctness of our code and helps reduce regression. Unit testing helps test components in isolation without any external dependency. Unit testing is not enough to ensure we have a stable well-tested system. In reality, failure happens during the integration of different components. Applications with database backend face the issue when we never ran our tests on a real database, we might never notice that things don’t work due to issues like transactions not committing, the wrong version of database etc. Integration testing plays an important role for the end to end testing.

In today’s world, we write a lot of software applications with databases as storage backend. Mocking these database calls for unit testing can be cumbersome. Making small changes in the schema can cause rewriting some or all mocks. Since queries don’t go to the actual database engine, there is no validation of query syntax or constraints. Need to mock each query can cause duplicate work. To avoid this, we should test with a real database which can be destroyed after testing is complete. Docker is perfect for running test cases as we can spin container in a few seconds and kill them when done.

Let’s understand how we can start a MySQL docker container and use it for testing using go code. We first need to make sure system running our test cases have docker installed which can be checked by running command “docker ps”. If docker is not installed, install docker from here.

func (d *Docker) isInstalled() bool {
command := exec.Command("docker", "ps")
err := command.Run()
if err != nil {
return false
}
return true
}

Once docker is installed we need to run MySQL container with a user and password which can be used to connect to MySQL server.

docker run --name our-mysql-container -e MYSQL_ROOT_PASSWORD=root -e MYSQL_USER=gouser -e MYSQL_PASSWORD=gopassword -e MYSQL_DATABASE=godb -p 3306:3306 --tmpfs /var/lib/mysql mysql:5.7

This runs a docker image of MySQL version 5.7 with the name of the container as “our-mysql-container”. “-e” specifies run-time variables we need to set for our MySQL docker container. We are setting root as our root password. Creating a user “gouser” with password “gopassword” which we use to connect to MySQL server from our application. We are exposing 3306 port of Docker container so we can connect to mysql server running inside docker container. We are using tmpfs mount which only stores data in the host machine’s memory. When the container stops, the tmpfs mount is removed. As we are running it for testing purpose so no need to store data in permanent storage.

type ContainerOption struct {
Name string
ContainerFileName string
Options map[string]string
MountVolumePath string
PortExpose string
}
func (d *Docker) getDockerRunOptions(c ContainerOption) []string {
portExpose := fmt.Sprintf("%s:%s", c.PortExpose, c.PortExpose)
var args []string
for key, value := range c.Options {
args = append(args, []string{"-e", fmt.Sprintf("%s=%s", key, value)}...)
}
args = append(args, []string{"--tmpfs", c.MountVolumePath, c.ContainerFileName}...)
dockerArgs := append([]string{"run", "-d", "--name", c.Name, "-p", portExpose}, args...)
return dockerArgs
}
func (d *Docker) Start(c ContainerOption) (string, error) {
dockerArgs := d.getDockerRunOptions(c)
command := exec.Command("docker", dockerArgs...)
command.Stderr = os.Stderr
result, err := command.Output()
if err != nil {
return "", err
}
d.ContainerID = strings.TrimSpace(string(result))
d.ContainerName = c.Name
command = exec.Command("docker", "inspect", d.ContainerID)
result, err = command.Output()
if err != nil {
d.Stop()
return "", err
}
return string(result), nil
}
func (m *MysqlDocker) StartMysqlDocker() {
mysqlOptions := map[string]string{
"MYSQL_ROOT_PASSWORD": "root",
"MYSQL_USER": "gouser",
"MYSQL_PASSWORD": "gopassword",
"MYSQL_DATABASE": "godb",
}
containerOption := ContainerOption{
Name: "our-mysql-container",
Options: mysqlOptions,
MountVolumePath: "/var/lib/mysql",
PortExpose: "3306",
ContainerFileName: "mysql:5.7",
}
m.Docker = Docker{}
m.Docker.Start(containerOption)
}

We can inspect container using containerId for details of the container.

docker inspect containerId

Once we run the Docker container, we need to wait till our docker container is up and running. We can check this using below command.

docker ps -a

Once docker is up and running we can start using it in our application for running integration test cases with a real database.

func (d *Docker) WaitForStartOrKill(timeout int) error {
for tick := 0; tick < timeout; tick++ {
containerStatus := d.getContainerStatus()
if containerStatus == dockerStatusRunning {
return nil
}
if containerStatus == dockerStatusExited {
return nil
}
time.Sleep(time.Second)
}
d.Stop()
return errors.New("Docker faile to start in given time period so stopped")
}
func (d *Docker) getContainerStatus() string {
command := exec.Command("docker", "ps", "-a", "--format", "{{.ID}}|{{.Status}}|{{.Ports}}|{{.Names}}")
output, err := command.CombinedOutput()
if err != nil {
d.Stop()
return dockerStatusExited
}
outputString := string(output)
outputString = strings.TrimSpace(outputString)
dockerPsResponse := strings.Split(outputString, "\n")
for _, response := range dockerPsResponse {
containerStatusData := strings.Split(response, "|")
containerStatus := containerStatusData[1]
containerName := containerStatusData[3]
if containerName == d.ContainerName {
if strings.HasPrefix(containerStatus, "Up ") {
return dockerStatusRunning
}
}
}
return dockerStatusStarting
}

We can use below connection string to connect to MySQL server running in docker from go code.

gouser:gopassword@tcp(localhost:3306)/godb?charset=utf8&parseTime=True&loc=Local

All this helps running integration testing with a real database which can be recreated on each run. This helps make sure our application is ready for production release.

The complete code can be found in this git repository: https://github.com/MiteshSharma/DockerMysqlGo

PS: If you liked the article, please support it with claps 👏. Cheers

--

--