0

I have a docker app which has two containers. One is MySql and the other is some logic code which I have created a custom image of using a Dockerfile. For end to end testing, I wish to store some values in the database and then run the logic code image (Logic in golang). This is the docker-compose file I have currently:

version: '3'
networks:
  docker-network:
    driver: bridge
services:
    database:
        image: mysql
        env_file:
          - ./src/logic/environment-variables.env
        ports:
          - 3306:3306
        healthcheck:
          test: "mysql -uroot -p$$MYSQL_ROOT_PASSWORD $$MYSQL_DATABASE -e 'select 1'"
          timeout: 20s
          retries: 10
        network:
          docker-network
    logic:
        container_name: main-logic
        build: ./src/logic/.
        depends_on:
          database:
            condition: service_healthy
        network:
          docker-network

I cannot run this app as a whole as that would run the main as soon as the db is running. Instead, I want to start the db, store some values in it, then run the logic image. How can I do this in a test method?

Approaches considered: Start up the mysql image separately from the test method and then store values in it.Then start the logic image and check the database for results. Is there a better way or a framework to use for this?

twothreezarsix
  • 375
  • 3
  • 13
  • Do you want to initialize correctly your database before of the start of your golang app? – JRichardsz Dec 22 '21 at 04:51
  • Yes. Basically need the db to have values in it so that I can test the code in the logic image – twothreezarsix Dec 22 '21 at 04:55
  • You can create a custom entrypoint.sh script for your MySQL container to run a migration and build/populate your database ... OR, if you aren't using any mysql specific functions, abstract the driver-type (IE: so it can be variable) and use a sqlite database on disk (IE: put it in a `testdata/` directiory) and use that to test your DB code ... or just mock the DB driver – John Dec 22 '21 at 06:53
  • Does this answer your question? [Does docker-compose support init container?](https://stackoverflow.com/questions/70322031/does-docker-compose-support-init-container) – The Fool Dec 22 '21 at 12:25
  • @TheFool this does in a way, but the question requires the feature even in actual situation, and not just for testing. But for my use case, I will need it only for testing and not when I run the code in other cases. If I use that, then the docker-compose contains unnecessary overhead in all cases for me – twothreezarsix Dec 22 '21 at 14:28

3 Answers3

0

What you need here are database migrations. That should work as follows :

  • Start DB instance before starting the service.
  • Connect the service to DB.
  • Run migrations on DB.
  • Continue with the service execution.

Consider this : https://github.com/golang-migrate/migrate

advay rajhansa
  • 1,129
  • 9
  • 17
0

For your approach:

  1. Start MySQL image.
  2. Upload data to the database.
  3. Start the logic image.
  4. Check the database for results.

You can:

Use Makefile

with a sh script inside, that will execute all steps one by one.

Makefile:

start_test:
    docker-compose run -d database
    # put here your data uploading script
    docker-compose run -d logic
    #  put here your data database checking script

Then execute

$make start_test # execute all steps

Use Testcontainers-Go

Testcontainers GitHub

Testcontainers-Go is a Go package that makes it simple to create and clean up container-based dependencies for automated integration/smoke tests.

It allows you to execute all steps in a go test method. For your case you will have something like this:

just a draft code to catch up the idea:

package main

import (
    "context"
    "database/sql"
    "fmt"
    "github.com/pkg/errors"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
    "log"
    "testing"
)

var db *sql.DB

func TestIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
    
    err := setupMySql()
    if err != nil {
        t.Errorf("Test failed with error: %s", err)
    }
    
    err = setupData()
    if err != nil {
        t.Errorf("Test failed with error: %s", err)
    }
    err = setupLogic()
    if err != nil {
        t.Errorf("Test failed with error: %s", err)
    }
    err = checkResult()
    if err != nil {
        t.Errorf("Test failed with error: %s", err)
    }
}

func setupMySql() error {
    ctx := context.Background()
    req := testcontainers.ContainerRequest{
        Image:        "mysql:latest",
        ExposedPorts: []string{"3306/tcp", "33060/tcp"},
        Env: map[string]string{
            "MYSQL_ROOT_PASSWORD": "secret",
        },
        WaitingFor: wait.ForLog("port: 3306  MySQL Community Server - GPL"),
    }
    mysqlC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: req,
        Started:          true,
    })
    defer func() {
        err := mysqlC.Terminate(ctx)
        if err != nil {
            log.Fatal(err)
        }
    }()
    
        if err != nil {
            return errors.Wrap(err, "Failed to run test container")
        }
    
    host, err := mysqlC.Host(ctx)
    p, err := mysqlC.MappedPort(ctx, "3306/tcp")
    port := p.Int()
    connectionString := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?tls=skip-verify",
        "root", "secret", host, port, "database")
    
    db, err = sql.Open("mysql", connectionString)
    defer func(db *sql.DB) {
        err := db.Close()
        if err != nil {
            log.Fatal(err)
        }
    }(db)
    
    if err != nil {
        return errors.Wrap(err, "Failed to connect to db")
    }
    
    return nil
}

func setupData() error {
    // db.Query(), your code with uploading data
    return nil
}

func setupLogic() error {
    // run your logic container
    return nil
}

func checkResult() error {
    // db.Query(), your code with checking result
    return nil
}

Use Dockertest

Dockertest helps you boot up ephermal docker images for your Go tests with minimal work.

Same as Testcontainers-Go,

just a draft code to catch up the idea:

package main

import (
    "database/sql"
    "fmt"
    "github.com/ory/dockertest/v3"
    "github.com/pkg/errors"
    "testing"
)

var db *sql.DB

func TestIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
    
    err := setupMySql()
    if err != nil {
        t.Errorf("Test failed with error: %s", err)
    }
    
    err = setupData()
    if err != nil {
        t.Errorf("Test failed with error: %s", err)
    }
    err = setupLogic()
    if err != nil {
        t.Errorf("Test failed with error: %s", err)
    }
    err = checkResult()
    if err != nil {
        t.Errorf("Test failed with error: %s", err)
    }
}

func setupMySql() error {
    // uses a sensible default on windows (tcp/http) and linux/osx (socket)
    pool, err := dockertest.NewPool("")
    if err != nil {
        return errors.Wrap(err, "Could not connect to docker")
    }
    
    // pulls an image, creates a container based on it and runs it
    resource, err := pool.Run("mysql", "5.7", []string{"MYSQL_ROOT_PASSWORD=secret"})
    if err != nil {
        return errors.Wrap(err, "Could not start resource")
    }
    
    // exponential backoff-retry, because the application in the container might not be ready to accept connections yet
    if err := pool.Retry(func() error {
        var err error
        db, err = sql.Open("mysql", fmt.Sprintf("root:secret@(localhost:%s)/mysql", resource.GetPort("3306/tcp")))
        if err != nil {
            return err
        }
        return db.Ping()
    }); err != nil {
        return errors.Wrap(err, "Could not connect to database")
    }
    
    if err := pool.Purge(resource); err != nil {
        return errors.Wrap(err, "Could not purge resource")
    }
    
    return nil
}

func setupData() error {
    // db.Query(), your code with uploading data
    return nil
}

func setupLogic() error {
    // run your logic container
    return nil
}

func checkResult() error {
    // db.Query(), your code with checking result
    return nil
}
Danil Perestoronin
  • 1,063
  • 1
  • 7
  • 9
0

You can do exactly what you say in the question: start the database, manually load the seed data, and start the rest of the application. Since your database has published ports: you can connect to it directly from the host without doing anything special.

docker-compose up -d database
mysql -h 127.0.0.1 < seed_data.sql
docker-compose up -d

@advayrajhansa's answer suggests using a database-migration system. If this was built into your image, you could docker-compose run logic migrate ... as the middle step. This runs an alternate command on the container you otherwise have defined in the docker-compose.yml file.

David Maze
  • 130,717
  • 29
  • 175
  • 215
  • I will try this. Does this mean the whole test method can be run as a script and does not require any go test funcs? – twothreezarsix Dec 22 '21 at 14:29
  • If it's the sort of end-to-end integration test that makes HTTP requests to your application and evaluates the outputs, it doesn't need to run inside a container or be written in the same language as the application. But that seems like a little bit different question from "how do I set up the initial data for that test". – David Maze Dec 22 '21 at 14:36