integration

package
v2.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 18, 2024 License: Apache-2.0 Imports: 16 Imported by: 0

README

Integration

This package contains helper functions that support integration testing of Go services and applications. The main library utilities are listed below.

  • Starting and stopping Docker containers inside Go tests
  • Creating JSON Web Tokens for testing gRPC and REST requests
  • Launching a Postgres database to tests against
  • Building and executing Go binaries

Examples

Start and Stop Docker Containers

Testing your application or service in isolation might be impossible. Your project may require a database or supporting services in order to function reliably.

Enter Docker containers.

You can prop-up Docker containers to substitute backend databases or services. This package is intended to make that process easier with helper functions. To start out, you might want to familiarize yourself with Go's built-in TestMain function, but here's the basic gist.

It is sometimes necessary for a test program to do extra setup or teardown before or after testing. It is also sometimes necessary for a test to control which code runs on the main thread. TestMain runs in the main goroutine and can do whatever setup and teardown is necessary.

Awesome. Example time!

import (
	"log"
	"testing"

	"github.com/infobloxopen/atlas-app-toolkit/integration"
)

// TestMain does pre-test set up
func TestMain(m *testing.M) {
  // RunContainer takes a docker image, docker run arguments, and 
  // runtime-specific arguments
  stop, err := integration.RunContainer(
    "redis:latest",
    []string{
      "--publish=6380:6380",
      "--env=REDIS_PASSWORD=password",
      "--rm",
    },
    []string{
      "maxmemory 2mb",
    },
  )
  if err != nil {
    log.Fatal("unable to start test redis container")
  }
  // stop and remove container after testing
  defer stop() 
  m.Run()
}
Launching a Postgres Database

Your application or service might be backed by a Postgres database. This library can help configure, launch, and reset a Postgres database between tests.

To start out, here's how you can use the Postgres options to configure your database.

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

	"github.com/infobloxopen/atlas-app-toolkit/integration"
	_ "github.com/lib/pq"
)

var (
	myTestDatabase integration.TestPostgresDB
)

func TestMain(m *testing.M) {
	// myMigrateFunc describes how to build the database schema from scratch
	myMigrateFunc := func(db *sql.DB) error {
		// migration.Run(db) isn't a real function, but let's pretend it
		// creates some tables in a database
		return migration.Run(db)
	}
	config, err := integration.NewTestPostgresDB(
		integration.WithName("my_database_name"),
		// passing a migrate up function will allow the database schema to be
		// destroyed and re-built, effectively causing the database to reset
		integration.WithMigrateUpFunction(myMigrateFunc),
	)
	if err != nil {
		log.Fatal("unable to build postgres config")
	}
	myTestDatabase = config
	stop, err := myTestDatabase.RunAsDockerContainer()
	if err != nil {
		log.Fatal("unable to start test database")
	}
	defer stop()
	m.Run()
}

The example above will configure and launch the database. The code below shows how to reset the database between tests.

import (
  "testing"
)

func TestMyEndpoint(t *testing.T) {
  // reset the database before running tests. you would want to do this if any
  // other tests create or modify entries in the database
  if err := myTestDatabase.Reset(); err != nil{
    t.Fatalf("unable to reset database schema: %v", err)
  }
  ... 
}
Building and Running Go Binaries

If you want to test your Go application or service, you'll need to build it first. The integration package provides helpers that enable you to build your Go binary and run it locally.

Alternatively, you can build your application or service's Docker image, then run the Docker image by following the [earlier examples](#Start and Stop Docker Containers).

Building the Binary
import (
	"log"
	"testing"

	"github.com/infobloxopen/atlas-app-toolkit/integration"
)

func TestMain(m *testing.M) {
  remove, err := integration.BuildGoSource("./path/to/my/go/package", "binaryName")
  if err != nil {
    log.Fatalf("unable to build go binary: %v", err)
  }
  // this will delete the binary after the tests run
  defer remove()
  m.Run()
}
Running the Binary
import (
	"log"
	"testing"

	"github.com/infobloxopen/atlas-app-toolkit/integration"
)

func TestMain(m *testing.M) {
	stop, err := integration.RunBinary(
		"./path/to/my/go/package/binaryName",
		// provide as many command-line arguments as you like
		"-debug=true",
	)
	if err != nil {
		log.Fatalf("unable to run go binary: %v", err)
	}
	// this will stop the running process
	defer stop()
	m.Run()
}

Finding Open Ports

To help avoid port conflicts, the integration package provides a simple helper that finds an port on the testing machine.

import (
	"log"
	"testing"

	"github.com/infobloxopen/atlas-app-toolkit/integration"
)

func TestMain(m *testing.M) {
	port, err := integration.GetOpenPort()
	if err != nil {
		log.Fatalf("unable to find open port: %v", err)
	}
}

You can also specify a port range.

import (
	"log"
	"testing"

	"github.com/infobloxopen/atlas-app-toolkit/integration"
)

func TestMain(m *testing.M) {
	port, err := integration.GetOpenPortInRange(6000, 8000)
	if err != nil {
		log.Fatalf("unable to find open port: %v", err)
	}
}
Creating JSON Web Tokens

If you plan to run test requests against your application or service, you might need to provide a JWT for authentication purposes. This isn't terribly tricky, but it's nice to have some helpers that spare you from reinventing the wheel.

Using the Standard Token

If you just need a token, but don't particularly care what it contains, then you might want to use the standard token. The term standard just means the token has the minimum required JWT claims that are needed to authenticate.

import (
	"net/http"
	"testing"

	"github.com/infobloxopen/atlas-app-toolkit/integration"
)

func TestMyEndpoint(t *testing.T) {
	token, err := integration.StandardTestJWT()
	if err != nil {
		t.Fatalf("unable to generate test token: %v", err)
	}
	req, err := http.NewRequest(http.MethodGet, "/endpoint", nil)
	if err != nil {
		t.Fatalf("unable to generate test request: %v", err)
	}
	req.Header.Set("Authorization: Bearer %s", token)
	...
}
Creating Default Test Requests

You might want to create REST requests or gRPC requests that use the standard JWT. Rather than write code that packs the JWT into the HTTP request header, or the gRPC request context, the integration library has utilities to do this for you.

Here's how you would be a test HTTP request.

import (
	"net/http"
	"testing"

	"github.com/infobloxopen/atlas-app-toolkit/integration"
)

func TestMyEndpoint(t *testing.T) {
	client := http.Client{}
	req, err := integration.MakeStandardRequest(
		http.MethodGet, "/endpoint", map[string]string{
			"message": "hello world",
		},
	)
	if err != nil {
		t.Fatalf("unable to build test http request: %v", err)
	}
	res, err := client.Do(req)
	...
}

And the same for gRPC requests.

import (
	"testing"

	"github.com/infobloxopen/atlas-app-toolkit/integration"
)

func TestMyGRPCEndpoint(t *testing.T) {
	ctx, err := integration.StandardTestingContext()
	if err != nil {
		t.Fatalf("unable to build test grpc context: %v", err)
	}
	gRPCResponseMessage, err := gRPCClient.MyGRPCEndpoint(ctx, gRPCRequestMessage)
	if err != nil {
		t.Fatalf("unable to send grpc request: %v", err)
	}
	...
}

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// StandardClaims is the standard payload inside a test JWT
	StandardClaims = jwt.MapClaims{
		auth.MultiTenancyField: "TestAccount",
	}
)

Functions

func AppendTokenToOutgoingContext

func AppendTokenToOutgoingContext(ctx context.Context, fieldName, token string) context.Context

AppendTokenToOutgoingContext adds an authorization token to the gRPC request context metadata. The user must provide a token field name like "token" or "bearer" to this function. It is intended specifically for gRPC testing.

Example (Output)
// make the jwt
authToken, err := jwt.NewWithClaims(
	jwt.SigningMethodHS256, jwt.MapClaims{
		"user":  "user-test",
		"roles": "admin",
	},
).SignedString([]byte("some-secret"))
if err != nil {
	log.Fatalf("unable to build token: %v", err)
}
// add the jwt to context
ctxBearer := AppendTokenToOutgoingContext(
	context.Background(), "Bearer", authToken,
)
// check to make sure the token was added
md, ok := metadata.FromOutgoingContext(ctxBearer)
if !ok || len(md["authorization"]) < 1 {
	log.Fatalf("unable to get token from context: %v", err)
}
fields := strings.Split(md["authorization"][0], " ")
if len(fields) < 2 {
	log.Fatalf("unexpected authorization metadata: %v", fields)
}
fmt.Println(fields[0] == "Bearer")
fmt.Println(fields[1] == authToken)
Output:

true
true

func BuildGoSource

func BuildGoSource(packagePath, output string) (func() error, error)

BuildGoSource builds a target Go package and gives the resulting binary some user-defined name. The function returned by BuildGoSource will remove the binary that got created.

func GetOpenPort

func GetOpenPort() (int, error)

GetOpenPort searches for an open port on the host

func GetOpenPortInRange

func GetOpenPortInRange(lowerBound, upperBound int) (int, error)

GetOpenPortInRange finds the first unused port within specific range

func MakeStandardRequest

func MakeStandardRequest(method, url string, payload interface{}) (*http.Request, error)

MakeStandardRequest issues an HTTP request a specific endpoint with Atlas-specific request data (e.g. the authorization token)

func MakeTestJWT

func MakeTestJWT(method jwt.SigningMethod, claims jwt.Claims) (string, error)

MakeTestJWT generates a token string based on the given JWT claims

func RunBinary

func RunBinary(binPath string, args ...string) (func(), error)

RunBinary runs a target binary with a set of arguments provided by the user

func RunContainer

func RunContainer(image string, dockerArgs, runtimeArgs []string) (func() error, error)

RunContainer launches a detached docker container on the host machine. It takes an image name, a list of "docker run" arguments, and a list of arguments that get passed to the container runtime

func StandardTestJWT

func StandardTestJWT() (string, error)

StandardTestJWT builds a JWT with the standard test claims in the JWT payload

func StandardTestingContext

func StandardTestingContext() (context.Context, error)

StandardTestingContext returns an outgoing request context that includes the standard test JWT. It is intended specifically for gRPC testing.

func WithMigrateDownFunction

func WithMigrateDownFunction(migrateDownFunction func(*sql.DB) error) func(*PostgresDB)

WithMigrateFunction is used to tear down the test Postgres according to a specific set of migrations. It runs on a per-test basis whenever the Reset() function is called.

func WithMigrateUpFunction

func WithMigrateUpFunction(migrateUpFunction func(*sql.DB) error) func(*PostgresDB)

WithMigrateFunction is used to rebuild the test Postgres database on a per-test basis. Whenever the database is reset with the Reset() function, the migrateUp function will rebuild the tables.

func WithName

func WithName(name string) func(*PostgresDB)

WithName is used to specify the name of the test Postgres database. By default the database name is "test-postgres-db"

func WithPassword

func WithPassword(password string) func(*PostgresDB)

WithPassword is used to specify the password of the test Postgres database

func WithPort

func WithPort(port int) func(*PostgresDB)

WithPort is used to specify the port of the test Postgres database. By default, the test database will find the first open port in the 35000+ range

func WithTimeout

func WithTimeout(timeout time.Duration) func(*PostgresDB)

WithTimeout is used to specify a connection timeout to the database

func WithUser

func WithUser(user string) func(*PostgresDB)

WithUser is used to specify the name of the Postgres user that owns the test database

func WithVersion

func WithVersion(version string) func(*PostgresDB)

WithVersion is used to specify the version of the test Postgres database. By default the version is "latest"

Types

type PostgresDB

type PostgresDB struct {
	// contains filtered or unexported fields
}

func NewTestPostgresDB

func NewTestPostgresDB(opts ...option) (PostgresDB, error)

NewTestPostgresDB returns a test postgres database that the functional options that have been provided by the caller

func (PostgresDB) CheckConnection

func (db PostgresDB) CheckConnection() error

func (PostgresDB) GetDSN

func (db PostgresDB) GetDSN() string

GetDSN returns the database connection string for the test Postgres database

func (PostgresDB) GetDriverName added in v2.2.0

func (db PostgresDB) GetDriverName() string

GetDriverName returns the name of the driver used for the DSN.

func (PostgresDB) Reset

func (db PostgresDB) Reset() error

Reset drops all the tables in a test database and regenerates them by running migrations. If a migration function has not been specified, then the tables are dropped but not regenerated

func (PostgresDB) RunAsDockerContainer

func (db PostgresDB) RunAsDockerContainer() (func() error, error)

RunAsDockerContainer spins-up a Postgres database server as a Docker container. The test Postgres database will run inside this Docker container.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL