Testcontainers for Go with Dolt

16 min read

Recently, a customer reached out to our team asking for a blog using Dolt with Testcontainers, "an open source framework for providing throwaway, lightweight instances of databases, message brokers, web browsers, or just about anything that can run in a Docker container."

I'd not encountered Testcontainers prior to this request, but was excited to investigate using Dolt with this framework, as well as showing off some of the unique and powerful features of Dolt that make it a compelling testing, and production, database.

For those who don't know, if MySQL and Git had a baby, it would be Dolt—a version controlled SQL database with cloning, branching, and merging that can be used as a drop in replacement for a MySQL instance.

Those familiar with running tests against traditional databases know that it's common practice to carefully use SQL transactions to manage the state of the database since tests mutate the database with edits. Most often transactions will be rolled back after a single test finishes so that a subsequent test begins with the desired database state.

What's cool about Dolt is that you can avoid having to manage the SQL transactions in your test framework altogether by simply executing each test on its own database branch.

Like a branch in Git, branches in Dolt contain isolated, independent working sets where changes exist only on the branch itself until they are committed and merged into other branches. One simply needs to create a new branch from the Dolt commit that contains the desired starting database state.

Additionally, because Dolt databases can be cloned like Git repositories can be cloned, setting up the initial state of a database is easy as well.

Traditional databases are typically seeded with mock data from a SQL file or some generated data from seeding code. Often this data does not reflect production data since actual production data can be so difficult to get into the testing environment.

With Dolt, though, getting production data into a testing environment is trivial. You simply need a copy of the production data hosted on a remote, like DoltHub, and then the test database can clone it locally, making it ready to test against.

To demonstrate this I created a repository showcasing these cool features of Dolt and how Testcontainers makes testing incredibly easy.

Getting Started with Testcontainers for Go and Dolt

When I discovered Testcontainers I followed their post Getting Started with Testcontainers for Go which creates an example repository that runs tests against PostgreSQL.

In the example repo, they use the built-in PostgreSQL module API to create and destroy the PostgreSQL Testcontainer. They also provide a GenericContainer implementation which is useful for setting up containers that aren't officially supported, which one could do for Dolt, but, we thought it would be better for Dolt to be an officially supported Testcontainers module 🤠.

So, I opened a pull request that's currently in review, that adds official support for Dolt, at least in Golang.

Next, I, created a similar sample repo from the Testcontainers article, but used the new Dolt module API from my pull request in place of PostgreSQL. I also changed the testing harness to leverage Dolt's unique branching and cloning features. Let's dive in!

Create Customer and Repository

Like the original article instructs, we start out with a Customer and Repository definition in their respective files, customer/types.go and customer/repo.go.

// customer/types.go

package customer

type Customer struct {
	Id    int
	Name  string
	Email string
}
// customer/repo.go

package customer

import (
	"context"
	"database/sql"
	"errors"
	// Import mysql into the scope of this package (required)
	_ "github.com/go-sql-driver/mysql"
)

type Repository struct {
	db *sql.DB
}

func NewRepository(ctx context.Context, db *sql.DB) (*Repository, error) {
	return &Repository{
		db: db,
	}, nil
}

func (r Repository) CreateCustomer(ctx context.Context, customer *Customer) (*Customer, error) {
	res, err := r.db.ExecContext(ctx, "INSERT INTO customers (name, email) VALUES (?, ?);", customer.Name, customer.Email)
	if err != nil {
		return nil, err
	}
	id, err := res.LastInsertId()
	if err != nil {
		return nil, err
	}
	customer.Id = int(id)
	return customer, nil
}

func (r Repository) GetCustomerByEmail(ctx context.Context, email string) (*Customer, error) {
	customer := Customer{}
	row := r.db.QueryRowContext(ctx, "SELECT id, name, email FROM customers WHERE email = ?;", email)
	err := row.Scan(&customer.Id, &customer.Name, &customer.Email)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	return &customer, nil
}

func (r Repository) UpdateCustomer(ctx context.Context, customer *Customer) error {
	_, err := r.db.ExecContext(ctx, "UPDATE customers SET name = ?, email = ? WHERE id = ?;", customer.Name, customer.Email, customer.Id)
	return err
}

func (r Repository) DeleteCustomer(ctx context.Context, customer *Customer) error {
	_, err := r.db.ExecContext(ctx, "DELETE from customers WHERE id = ?;", customer.Id)
	return err
}

Customer is straightforward, simplified customer data and Repository is the interface for accessing the underlying database, Dolt, instead of PostgreSQL.

One change I made from the original sample repo was to change the NewRepository arguments to accept a *sql.DB instead of a connection string. I did this so that the Repository would not need to handle any configuration of the database connection, and would simple operate against any *sql.DB it received.

This change was necessary to have each test executing on a different branch, but we'll cover that in more detail shortly.

I also added UpdateCustomer and DeleteCustomer Repository methods to better illustrate that tests can modify the database freely and run in any order, without affecting any other tests. A test could even drop the customers table and that change would only occur on the distinct branch for that specific test 🤯!

Add Seeding Scripts

The next step the article has you do is add the script testdata/init-db.sql that runs during database initialization and seeds the database with some data. In the PostgreSQL sample repo, it creates a customers table and inserts a single customer "John".

We add a similar script in our sample repo as well, called testdata/init-db.sql that contains:

CREATE DATABASE IF NOT EXISTS test_db;

USE test_db;

CREATE TABLE IF NOT EXISTS customers (id serial, name varchar(255), email varchar(255));

CALL DOLT_COMMIT('-Am', 'create table customers');

INSERT INTO customers(name, email) VALUES ('John', 'john@gmail.com');

CALL DOLT_COMMIT('-Am', 'insert john into customers');

First, notice we must create the database in our initialization script, whereas the script for PostgreSQL did not. This is because the dolthub/dolt-sql-server Docker image does not create a database for us by default, so we do so in the initialization script.

Next, we create the same customers table and insert "John" into the table, but after each step we also create Dolt commits using CALL DOLT_COMMIT. This ensures that our main branch of the database, which will be the parent of each new testing branch, will have the same data when each test runs.

But what if we don't want to create a new database, and instead want to clone an existing one to test?

We can do so by adding a different initialization script called testdata/clone-db.sh which contains:

#!/bin/bash

# use credentials for remote
if [ -n "$DOLT_CREDS_PUB_KEY" ]; then
  dolt creds use "$DOLT_CREDS_PUB_KEY"
fi

# clone
dolt sql -q "CALL DOLT_CLONE('$DOLT_REMOTE_CLONE_URL', '$DOLT_DATABASE');"

This file allows us to do exactly that by initializing our Dolt container using CALL DOLT_CLONE, which clones the database to the server.

If the remote database is private, we can also supply a Dolt credentials file and public key to the Dolt Testcontainer to authorize the Dolt server to clone it.

Once we have these two initialization scripts, it's time to create our Dolt Testcontainers that use them.

Define Dolt Testcontainers

Skipping to the Reusing the containers and running multiple tests as a suite section of the article, it tells you to refactor some previous code and create a new file called testhelpers/containers.go which contains the instantiation of the PostgreSQL Testcontainer.

We'll add this same file in our repo, but this time, it will contain the Testcontainer setup for our two Dolt containers:

// testhelpers/containers.go

package testhelpers

import (
	"context"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/modules/dolt"
	"path/filepath"
)

type DoltContainer struct {
	*dolt.DoltContainer
	ConnectionString string
}

func CreateDoltContainer(ctx context.Context) (*DoltContainer, error) {
	doltContainer, err := dolt.RunContainer(ctx,
		testcontainers.WithImage("dolthub/dolt-sql-server:latest"),
		dolt.WithScripts(filepath.Join("..", "testdata", "init-db.sql")),
		dolt.WithDatabase("test_db"),
		dolt.WithUsername("tester"),
		dolt.WithPassword("testing"),
	)
	if err != nil {
		return nil, err
	}

	connStr, err := doltContainer.ConnectionString(ctx)
	if err != nil {
		return nil, err
	}

	return &DoltContainer{
		DoltContainer:    doltContainer,
		ConnectionString: connStr,
	}, nil
}

func CreateDoltContainerFromClone(ctx context.Context) (*DoltContainer, error) {
	doltContainer, err := dolt.RunContainer(ctx,
		testcontainers.WithImage("dolthub/dolt-sql-server:latest"),
		dolt.WithScripts(filepath.Join("..", "testdata", "clone-db.sh")),
		dolt.WithDatabase("test_db"),
		dolt.WithUsername("tester"),
		dolt.WithPassword("testing"),
		dolt.WithDoltCloneRemoteUrl("https://doltremoteapi.dolthub.com/dolthub/testcontainers-go-demo"),
	)
	if err != nil {
		return nil, err
	}

	connStr, err := doltContainer.ConnectionString(ctx)
	if err != nil {
		return nil, err
	}

	return &DoltContainer{
		DoltContainer:    doltContainer,
		ConnectionString: connStr,
	}, nil
}

Above you'll see that CreateDoltContainer creates a DoltContainer initialized with the testdata/init-db.sql script, so will only have "John" in the customers table.

And you'll also see CreateDoltContainerFromClone which uses the testdata/clone-db.sh to clone this DoltHub database during initialization. This remote database does not contain "John", but instead contains "Vicki", "Megan", and "Lisa".

We'll use these different DoltContainers to run two test suites, one against the locally seeded Dolt database and one against the remotely seeded Dolt database.

Create Test Suites

Looking back at the original article, we next need to create a file called customer/repo_suite_test.go that contains the testify suite implementation and some tests defined on the CustomerRepoTestSuite.

For our locally seeded database will define a similar test suite, but for readability we'll define the tests in a separate file called customer/repo_test.go.

// customer/repo_test_suite.go

package customer

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"testing"

	"github.com/dolthub/testcontainers-go-demo/testhelpers"
	"github.com/stretchr/testify/suite"
)

type CustomerRepoTestSuite struct {
	suite.Suite
	doltContainer *testhelpers.DoltContainer
	repository    *Repository
	db            *sql.DB
	ctx           context.Context
	startRef      string
}

func (suite *CustomerRepoTestSuite) SetupSuite() {
	suite.ctx = context.Background()
	doltContainer, err := testhelpers.CreateDoltContainer(suite.ctx)
	if err != nil {
		log.Fatal(err)
	}

	suite.doltContainer = doltContainer
	if suite.startRef == "" {
		suite.startRef = "main"
	}
}

func (suite *CustomerRepoTestSuite) TearDownSuite() {
	if err := suite.doltContainer.Terminate(suite.ctx); err != nil {
		log.Fatalf("error terminating dolt container: %s", err)
	}
}

func (suite *CustomerRepoTestSuite) createBranchName(suiteName, testName string) string {
	return fmt.Sprintf("%s_%s", suiteName, testName)
}

func (suite *CustomerRepoTestSuite) AfterTest(suiteName, testName string) {
	if suite.db != nil {
		branchName := suite.createBranchName(suiteName, testName)
		_, err := suite.db.ExecContext(suite.ctx, "CALL DOLT_COMMIT('-Am', ?)", fmt.Sprintf("Finished testing on %s", branchName))
		if err != nil {
			if !strings.Contains(err.Error(), "nothing to commit") {
				log.Fatal(err)
			}
		}
		err = suite.db.Close()
		if err != nil {
			log.Fatal(err)
		}
		suite.db = nil
		suite.repository = nil
	}
}

func (suite *CustomerRepoTestSuite) BeforeTest(suiteName, testName string) {
	// connect to the dolt server
	db, err := sql.Open("mysql", suite.doltContainer.ConnectionString)
	if err != nil {
		log.Fatal(err)
	}

	newBranch := suite.createBranchName(suiteName, testName)

	// checkout new branch for test from the designated starting point
	_, err = db.ExecContext(suite.ctx, "CALL DOLT_CHECKOUT(?, '-b', ?);", suite.startRef, newBranch)
	if err != nil {
		log.Fatal(err)
	}

	suite.db = db

	repository, err := NewRepository(suite.ctx, suite.db)
	if err != nil {
		log.Fatal(err)
	}

	suite.repository = repository
}

func TestCustomerRepoTestSuite(t *testing.T) {
	suite.Run(t, new(CustomerRepoTestSuite))
}

In our version of CustomerRepoTestSuite, the SetupSuite method, called at the start of a test suite run, uses testhelpers.CreateDoltContainer to create a Dolt Testcontainer from testdata/init-db.sql. TearDownSuite, run after the test suite completes, terminates the Dolt Testcontainer, cleaning up any resources it used.

BeforeTest is the method we use on our suite to ensure that each test is run on a distinct Dolt branch. As its name suggests, this method, called before each test, opens a connection to the Dolt database, executes CALL DOLT_CHECKOUT to create and checkout a new branch for the given test, then instantiates a new Repository that will operate against that database connection on that new branch.

AfterTest runs after each test and attempts to commit any changes that occurred during testing. It then closes the open database connection before resetting the suite's db and repository fields. It's certainly possible to add additional logic to delete the testing branches after each test (and they will be deleted by Testcontainers anyway), but we can use these lingering branches later to do some debugging if need be. More on that later.

TestCustomerRepoTestSuite is what's called to run this test suite and all of its tests.

Here are the tests I've defined for this suite:

// customer/repo_test.go

package customer

import "github.com/stretchr/testify/assert"

func (suite *CustomerRepoTestSuite) TestCreateCustomer() {
	t := suite.T()

	customer, err := suite.repository.CreateCustomer(suite.ctx, &Customer{
		Name:  "Henry",
		Email: "henry@gmail.com",
	})
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.NotNil(t, customer.Id)
}

func (suite *CustomerRepoTestSuite) TestGetCustomerByEmail() {
	t := suite.T()

	customer, err := suite.repository.GetCustomerByEmail(suite.ctx, "john@gmail.com")
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.Equal(t, "John", customer.Name)
	assert.Equal(t, "john@gmail.com", customer.Email)
}

func (suite *CustomerRepoTestSuite) TestUpdateCustomer() {
	t := suite.T()

	customer, err := suite.repository.GetCustomerByEmail(suite.ctx, "john@gmail.com")
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.Equal(t, "John", customer.Name)
	assert.Equal(t, "john@gmail.com", customer.Email)

	id := customer.Id
	customer.Name = "JohnAlt"
	customer.Email = "john-alt@gmail.com"
	err = suite.repository.UpdateCustomer(suite.ctx, customer)
	assert.NoError(t, err)

	customer, err = suite.repository.GetCustomerByEmail(suite.ctx, "john-alt@gmail.com")
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.Equal(t, "JohnAlt", customer.Name)
	assert.Equal(t, "john-alt@gmail.com", customer.Email)
	assert.Equal(t, id, customer.Id)
}

func (suite *CustomerRepoTestSuite) TestDeleteCustomer() {
	t := suite.T()

	customer, err := suite.repository.GetCustomerByEmail(suite.ctx, "john@gmail.com")
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.Equal(t, "John", customer.Name)
	assert.Equal(t, "john@gmail.com", customer.Email)

	err = suite.repository.DeleteCustomer(suite.ctx, customer)
	assert.NoError(t, err)

	customer, err = suite.repository.GetCustomerByEmail(suite.ctx, "john@gmail.com")
	assert.NoError(t, err)
	assert.Nil(t, customer)
}

They contain the original tests from the Testcontainers article but with UpdateCustomer and DeleteCustomer tests added.

Now let's create a test suite that uses the remote data from the database hosted on DoltHub.

In another file called customer/clone_suite_test.go define another test suite like so:

package customer

import (
	"context"
	"log"
	"testing"

	"github.com/dolthub/testcontainers-go-demo/testhelpers"
	"github.com/stretchr/testify/suite"
)

type RemoteRepoTestSuite struct {
	c *CustomerRepoTestSuite
}

func (suite *RemoteRepoTestSuite) T() *testing.T {
	return suite.c.T()
}

func (suite *RemoteRepoTestSuite) SetT(t *testing.T) {
	suite.c.SetT(t)
}

func (suite *RemoteRepoTestSuite) SetS(s suite.TestingSuite) {
	suite.c.SetS(s)
}

func (suite *RemoteRepoTestSuite) Ctx() context.Context {
	return suite.c.ctx
}

func (suite *RemoteRepoTestSuite) Repository() *Repository {
	return suite.c.repository
}

func (suite *RemoteRepoTestSuite) SetupSuite() {
	suite.c.ctx = context.Background()
	doltContainer, err := testhelpers.CreateDoltContainerFromClone(suite.c.ctx)
	if err != nil {
		log.Fatal(err)
	}

	suite.c.doltContainer = doltContainer
	if suite.c.startRef == "" {
		suite.c.startRef = "main"
	}
}

func (suite *RemoteRepoTestSuite) TearDownSuite() {
	suite.c.TearDownSuite()
}

func (suite *RemoteRepoTestSuite) AfterTest(suiteName, testName string) {
	suite.c.AfterTest(suiteName, testName)
}

func (suite *RemoteRepoTestSuite) BeforeTest(suiteName, testName string) {
	suite.c.BeforeTest(suiteName, testName)
}

func TestRemoteRepoTestSuite(t *testing.T) {
	s := &RemoteRepoTestSuite{c: new(CustomerRepoTestSuite)}
	suite.Run(t, s)
}

This test suite can simply wrap CustomerRepoTestSuite and in its SetupSuite method it can call testhelpers.CreateDoltContainerFromClone. The rest of the methods can simply call their underlying equivalent. That's basically it 😅.

Now we just need some tests that execute against this database. Add a file called customer/clone_test.go that contains:

// customer/clone.go

package customer

import "github.com/stretchr/testify/assert"

func (suite *RemoteRepoTestSuite) TestCreateCustomer() {
	t := suite.T()

	customer, err := suite.Repository().CreateCustomer(suite.Ctx(), &Customer{
		Name:  "Henry",
		Email: "henry@gmail.com",
	})
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.NotNil(t, customer.Id)
}

func (suite *RemoteRepoTestSuite) TestGetCustomerByEmail() {
	t := suite.T()

	customer, err := suite.Repository().GetCustomerByEmail(suite.Ctx(), "lisa@gmail.com")
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.Equal(t, "Lisa", customer.Name)
	assert.Equal(t, "lisa@gmail.com", customer.Email)
}

func (suite *RemoteRepoTestSuite) TestUpdateCustomer() {
	t := suite.T()

	customer, err := suite.Repository().GetCustomerByEmail(suite.Ctx(), "vicki@gmail.com")
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.Equal(t, "Vicki", customer.Name)
	assert.Equal(t, "vicki@gmail.com", customer.Email)

	id := customer.Id
	customer.Name = "Samantha"
	customer.Email = "samantha@gmail.com"
	err = suite.Repository().UpdateCustomer(suite.Ctx(), customer)
	assert.NoError(t, err)

	customer, err = suite.Repository().GetCustomerByEmail(suite.Ctx(), "samantha@gmail.com")
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.Equal(t, "Samantha", customer.Name)
	assert.Equal(t, "samantha@gmail.com", customer.Email)
	assert.Equal(t, id, customer.Id)
}

func (suite *RemoteRepoTestSuite) TestDeleteCustomer() {
	t := suite.T()

	customer, err := suite.Repository().GetCustomerByEmail(suite.Ctx(), "megan@gmail.com")
	assert.NoError(t, err)
	assert.NotNil(t, customer)
	assert.Equal(t, "Megan", customer.Name)
	assert.Equal(t, "megan@gmail.com", customer.Email)

	err = suite.Repository().DeleteCustomer(suite.Ctx(), customer)
	assert.NoError(t, err)

	customer, err = suite.Repository().GetCustomerByEmail(suite.Ctx(), "megan@gmail.com")
	assert.NoError(t, err)
	assert.Nil(t, customer)
}

These tests use the same methods as the other test suite, but can expect the data from the remote database instead of the data from the local database. Plus, you get the same isolated one branch per test guarantee without any extra coding.

Now let's say you need to do some debugging by investigating the state of the database for a particular failing test. To do this you can, first, keep the Dolt container alive by adding a time.Sleep() to the TearDownSuite() method so that the container is not actually torn down. Then, you'll need to connect to the Dolt container with a mysql client on the port used by Docker.

docker ps
CONTAINER ID   IMAGE                            COMMAND                  CREATED         STATUS         PORTS                                               NAMES
54101e344f2f   dolthub/dolt-sql-server:latest   "tini -- docker-entr…"   8 seconds ago   Up 8 seconds   0.0.0.0:64579->3306/tcp, 0.0.0.0:64580->33060/tcp   charming_colden
a407c7fb6826   testcontainers/ryuk:0.6.0        "/bin/ryuk"              9 seconds ago   Up 8 seconds   0.0.0.0:64574->8080/tcp                             reaper_b35ad4ba6ffb051b3e86910b96c2e5107ac275eddea49d6b43f2d7ca7955a272

Running docker ps will show that port 64579 is being forwarded to 3306 on the dolthub/dolt-sql-server container, so we can connect with:

 mysql --host 0.0.0.0 --port 64579 -uroot

From here, we can select our database called test_db and show the branches in that database that our tests made:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| test_db            |
+--------------------+
3 rows in set (0.02 sec)

mysql> use test_db;
mysql> select * from dolt_branches;
+----------------------------------------------+----------------------------------+------------------+------------------------+---------------------+--------------------------------------------------------------+--------+--------+
| name                                         | hash                             | latest_committer | latest_committer_email | latest_commit_date  | latest_commit_message                                        | remote | branch |
+----------------------------------------------+----------------------------------+------------------+------------------------+---------------------+--------------------------------------------------------------+--------+--------+
| CustomerRepoTestSuite_TestCreateCustomer     | 4hdjr4n4pj2ocuab0pkjd22sgh1do42v | tester           | tester@%               | 2024-02-01 00:30:30 | Finished testing on CustomerRepoTestSuite_TestCreateCustomer |        |        |
| CustomerRepoTestSuite_TestDeleteCustomer     | fm1vi42gs5a8v1m8j9eekd7p6fe9fl01 | tester           | tester@%               | 2024-02-01 00:30:30 | Finished testing on CustomerRepoTestSuite_TestDeleteCustomer |        |        |
| CustomerRepoTestSuite_TestGetCustomerByEmail | 35ppio7l1uod8irfbkp1uh4kcakrc99m | root             | root@localhost         | 2024-02-01 00:30:29 | insert john into customers                                   |        |        |
| CustomerRepoTestSuite_TestUpdateCustomer     | vokoftbvpe4altvbc08g052l26228350 | tester           | tester@%               | 2024-02-01 00:30:30 | Finished testing on CustomerRepoTestSuite_TestUpdateCustomer |        |        |
| main                                         | 35ppio7l1uod8irfbkp1uh4kcakrc99m | root             | root@localhost         | 2024-02-01 00:30:29 | insert john into customers                                   |        |        |
+----------------------------------------------+----------------------------------+------------------+------------------------+---------------------+--------------------------------------------------------------+--------+--------+
5 rows in set (0.02 sec)

Finally, we can view the diff created by the test simply by querying the dolt_commit_diff_$tablename table. Here's the diff created by the test TestDeleteCustomer:

mysql> select * from dolt_commit_diff_customers where to_commit = HASHOF('CustomerRepoTestSuite_TestDeleteCustomer') and from_commit = HASHOF('main');
+-------+---------+----------+----------------------------------+-------------------------+---------+-----------+----------------+----------------------------------+-------------------------+-----------+
| to_id | to_name | to_email | to_commit                        | to_commit_date          | from_id | from_name | from_email     | from_commit                      | from_commit_date        | diff_type |
+-------+---------+----------+----------------------------------+-------------------------+---------+-----------+----------------+----------------------------------+-------------------------+-----------+
|  NULL | NULL    | NULL     | fm1vi42gs5a8v1m8j9eekd7p6fe9fl01 | 2024-02-01 00:30:30.244 |       1 | John      | john@gmail.com | 35ppio7l1uod8irfbkp1uh4kcakrc99m | 2024-02-01 00:30:29.225 | removed   |
+-------+---------+----------+----------------------------------+-------------------------+---------+-----------+----------------+----------------------------------+-------------------------+-----------+
1 row in set (0.04 sec)

"John" was removed by this test, as expected. And here is the diff for TestUpdateCustomer:

mysql> select * from dolt_commit_diff_customers where to_commit = HASHOF('CustomerRepoTestSuite_TestUpdateCustomer') and from_commit = HASHOF('main');
+-------+---------+--------------------+----------------------------------+-------------------------+---------+-----------+----------------+----------------------------------+-------------------------+-----------+
| to_id | to_name | to_email           | to_commit                        | to_commit_date          | from_id | from_name | from_email     | from_commit                      | from_commit_date        | diff_type |
+-------+---------+--------------------+----------------------------------+-------------------------+---------+-----------+----------------+----------------------------------+-------------------------+-----------+
|  NULL | NULL    | NULL               | vokoftbvpe4altvbc08g052l26228350 | 2024-02-01 00:30:30.307 |       1 | John      | john@gmail.com | 35ppio7l1uod8irfbkp1uh4kcakrc99m | 2024-02-01 00:30:29.225 | removed   |
|     1 | JohnAlt | john-alt@gmail.com | vokoftbvpe4altvbc08g052l26228350 | 2024-02-01 00:30:30.307 |    NULL | NULL      | NULL           | 35ppio7l1uod8irfbkp1uh4kcakrc99m | 2024-02-01 00:30:29.225 | added     |
+-------+---------+--------------------+----------------------------------+-------------------------+---------+-----------+----------------+----------------------------------+-------------------------+-----------+
2 rows in set (0.03 sec)

We can see that the update resulted in a removal of "John" and the addition of "JohnAlt", which is what we expected here as well.

You're now testing with Dolt and TestContainers in Go!

If you'd like to try testing your production MySQL database with Dolt and Testcontainers, you'll need to create a SQL dump of your database and add it to the Dolt container initialization scripts with dolt.WithScripts().

Alternatively, you can import the dump into a Dolt database hosted on either DoltHub or DoltLab, and follow the steps for cloning into a Dolt container described above.

Happy testing!

Conclusion

Hopefully you find this guide helpful, we'd love for you to give Dolt and Testcontainers a try. We're always on the hunt for different tools and integrations we can pair with Dolt that add value to our customers.

Feel free to come by our Discord and let us know how you're using Dolt and TestContainers, or if there are other products or tools you think make sense with Dolt.

Don't forget to check out each of our different product offerings below, to find which ones are right for you:

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.