skip to Main Content

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?

3

Answers


  1. 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

    Login or Signup to reply.
  2. 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
    }
    
    Login or Signup to reply.
  3. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search