Testing Go apps with Oracle Database using Testcontainers

In this article, we’ll walk through how to configure an Oracle Database Free container for go application tests using testcontainers-go.

What is Testcontainers?

Testcontainers is a popular framework that lets you test applications against real, disposable containers. It helps ensure your tests run against realistic environments without requiring complex infrastructure setup.

Testcontainers for Oracle Database (go)

The testcontainers-go module makes it simple to create and extend containers. Let’s write an implementation, oracle_container.go, that starts a database container with a configurable username and password:

package testcontainers

import (
	"context"
	"database/sql"
	"fmt"
	"github.com/anders-swanson/oracle-database-java-samples/golang/connection"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

const (
	containerPort     = "1521/tcp"
	containerLogReady = "DATABASE IS READY TO USE!"

	appUserEnvVar              = "APP_USER"
	appUserPasswordEnvVar      = "APP_USER_PASSWORD"
	oracleRandomPasswordEnvVar = "ORACLE_RANDOM_PASSWORD"
)

type OracleContainer struct {
	username string
	password string
	*testcontainers.DockerContainer
}

func NewOracleContainer(ctx context.Context, image, appUser, appUserPassword string, opts ...testcontainers.ContainerCustomizer) (*OracleContainer, error) {
	// Configure Oracle Container with default options
	opts = append(opts, testcontainers.WithExposedPorts(containerPort),
		testcontainers.WithWaitStrategy(
			wait.ForListeningPort(containerPort),
			wait.ForLog(containerLogReady),
		),
		testcontainers.WithEnv(map[string]string{
			oracleRandomPasswordEnvVar: "y",
			appUserEnvVar:              appUser,
			appUserPasswordEnvVar:      appUserPassword,
		}),
	)

	// Start the container
	container, err := testcontainers.Run(ctx,
		image,
		opts...,
	)
	if err != nil {
		return nil, err
	}
	return &OracleContainer{
		username:        appUser,
		password:        appUserPassword,
		DockerContainer: container,
	}, nil
}

func (o *OracleContainer) GetDB(ctx context.Context) (*sql.DB, error) {
	// Either set DYLD_LIBRARY_PATH to point to the Oracle client libraries,
	// or have the client libraries present on your system PATH.
	endpoint, err := o.Endpoint(ctx, "")
	if err != nil {
		return nil, err
	}
	return connection.NewDatabase(
		o.username,
		o.password,
		fmt.Sprintf("%s/freepdb1", endpoint),
	), nil
}

We also add a GetDB method to return a Go sql.db connected to the container. This method uses my NewDatabase function to create a basic connection pool, though you may configure the container connection pool as needed.

database_connection.go:

package connection

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/godror/godror"
)

func DefaultLocalhostConnection() *sql.DB {
	return NewDatabase("testuser", "testpwd", "localhost:1521/freepdb1")
}

func NewDatabase(username, password, url string) *sql.DB {
	// Either set DYLD_LIBRARY_PATH to point to the Oracle client libraries,
	// or have the client libraries present on your system PATH.
	ldLibraryPath := os.Getenv("DYLD_LIBRARY_PATH")
	if ldLibraryPath == "" {
		log.Fatalf("DYLD_LIBRARY_PATH unset, cannot find Oracle client libraries")
	}

	var P godror.ConnectionParams
	// If password is not specified, externalAuth will be true, and we'll ignore user input
	isExternalAuth := password == ""
	externalAuth := sql.NullBool{
		Bool:  isExternalAuth,
		Valid: true,
	}
	if isExternalAuth {
		username = ""
	}

	// Setup connection parameters
	P.Username = username
	P.Password = godror.NewPassword(password)
	P.ConnectString = url
	P.ExternalAuth = externalAuth

	// Connection pooling parameters
	P.PoolParams.WaitTimeout = time.Second * 5

	// Recommended to use minimal pooling,
	// otherwise you will manage a single connection
	P.PoolParams.SessionIncrement = 1
	P.PoolParams.MinSessions = 1
	P.PoolParams.MaxSessions = 15
	// See P.PoolParams for additional pooling configuration

	// Database TNS Admin
	tnsAdmin := os.Getenv("TNS_ADMIN")
	if tnsAdmin != "" {
		P.ConfigDir = tnsAdmin
	}

	// Optionally set database role
	// P.AdminRole = dsn.SysDBA

	// Create new database using connection parameters
	db := sql.OpenDB(godror.NewConnector(P))
	db.SetMaxOpenConns(15)
	db.SetMaxIdleConns(15)
	db.SetConnMaxLifetime(0)

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	if _, err := db.ExecContext(ctx, `
			begin
	       		dbms_application_info.set_client_info('oracledb_exporter');
			end;`); err != nil {
		fmt.Println("Could not set CLIENT_INFO.")
	}

	return db
}

Write a test for OracleContainer

We create a simple test that starts an Oracle Database Free container, gets a database connection, and queries the database version.

oracle_container_test.go:

package testcontainers

import (
	"context"
	"fmt"
	"github.com/stretchr/testify/assert"
	"testing"
	"time"
)

func TestOracleContainer(t *testing.T) {
	// Start an Oracle Database container
	ctx := context.Background()
	container, err := NewOracleContainer(ctx,
		"gvenzl/oracle-free:23.26.0-slim-faststart",
		"testuser",
		"testpwd",
	)
	assert.Nil(t, err)

	// Get a database connection for the container
	db, err := container.GetDB(ctx)
	assert.Nil(t, err)

	// Query the version from the database
	queryCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	var banner string
	err = db.QueryRowContext(queryCtx, "SELECT banner FROM v$version WHERE ROWNUM = 1").Scan(&banner)
	assert.Nil(t, err)
	assert.Equal(t, "Oracle Database 23ai Free Release 23.0.0.0.0 - Develop, Learn, and Run for Free", banner)
	fmt.Println(banner)
}

Run the test

To run the test, set the DYLD_LIBRARY_PATH environment variable so it points to the directory containing your Oracle Instant Client. If you don’t have an Oracle Instant Client library downloaded, get it Here.

The instantclient library is required because godror is a thick database client.

export DYLD_LIBRARY_PATH=/path/to/instantclient

From the golang directory, run the test using go test:

go test ./testcontainers

That’s it! The test will run, start the container, and then automatically clean up the database on completion.

References

Response

  1. […] Testing Go apps with Oracle Database using Testcontainers – Anders Swanson demonstrates how you can test Go applications with Oracle […]

Leave a Reply

Discover more from andersswanson.dev

Subscribe now to keep reading and get access to the full archive.

Continue reading