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?
CodePudding user response:
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
CodePudding user response:
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.
CodePudding user response:
For your approach:
- Start MySQL image.
- Upload data to the database.
- Start the logic image.
- 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-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
}