Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add container examples to documentation, clean up README #353

Merged
merged 14 commits into from
Oct 22, 2021
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ site/
src/mkdocs-codeinclude-plugin
src/pip-delete-this-directory.txt
.idea/
.DS_Store

cover.txt
67 changes: 42 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/testcontainers/testcontainers-go)](https://goreportcard.com/report/github.com/testcontainers/testcontainers-go)
[![GoDoc Reference](https://camo.githubusercontent.com/8609cfcb531fa0f5598a3d4353596fae9336cce3/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f79616e6777656e6d61692f686f772d746f2d6164642d62616467652d696e2d6769746875622d726561646d653f7374617475732e737667)](https://pkg.go.dev/github.com/testcontainers/testcontainers-go)

When I was working on a Zipkin PR I discovered a nice Java library called
[Testcontainers](https://www.testcontainers.org/).
Testcontainers-Go is a Go package that makes it simple to create and clean up container-based dependencies for
automated integration/smoke tests. The clean, easy-to-use API enables developers to programmatically define containers
that should be run as part of a test and clean up those resources when the test is done.

It provides an easy and clean API over the go docker sdk to run, terminate and
connect to containers in your tests.

I found myself comfortable programmatically writing the containers I need to run
an integration/smoke tests. So I started porting this library in Go.

This is an example:
Here's an example of a test that spins up an NGINX container validates that it returns 200 for the status code:

```go
package main
Expand All @@ -26,44 +21,66 @@ import (
"github.com/testcontainers/testcontainers-go/wait"
)

func TestNginxLatestReturn(t *testing.T) {
ctx := context.Background()
type nginxContainer struct {
testcontainers.Container
URI string
}

func setupNginx(ctx context.Context) (*nginxContainer, error) {
req := testcontainers.ContainerRequest{
Image: "nginx",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForHTTP("/"),
}
nginxC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatal(err)
return nil, err
}
defer nginxC.Terminate(ctx)
ip, err := nginxC.Host(ctx)

ip, err := container.Host(ctx)
if err != nil {
t.Fatal(err)
return nil, err
}
port, err := nginxC.MappedPort(ctx, "80")

mappedPort, err := container.MappedPort(ctx, "80")
if err != nil {
return nil, err
}

uri := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port())

return &nginxContainer{Container: container, URI: uri}, nil
}

func TestIntegrationNginxLatestReturn(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

ctx := context.Background()

nginxC, err := setupNginx(ctx)
if err != nil {
t.Fatal(err)
}
resp, err := http.Get(fmt.Sprintf("http://%s:%s", ip, port.Port()))

// Clean up the container after the test is complete
defer nginxC.Terminate(ctx)

resp, err := http.Get(nginxC.URI)
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode)
}
}
```
This is a simple example, you can create one container in my case using the
`nginx` image. You can get its IP `ip, err := nginxC.GetContainerIpAddress(ctx)` and you
can use it to make a GET: `resp, err := http.Get(fmt.Sprintf("http://%s", ip))`

To clean your environment you can defer the container termination `defer
nginxC.Terminate(ctx, t)`. `t` is `*testing.T` and it is used to notify is the
`defer` failed marking the test as failed.
Cleaning up your environment after test completion should be accomplished by deferring the container termination, e.g
`defer nginxC.Terminate(ctx)`. Reaper (Ryuk) is also enabled by default to help clean up.

## Documentation

The documentation lives in [./docs](./docs) and it is rendered at
More information about TestContainers-Go can be found in [./docs](./docs), which is rendered at
[golang.testcontainers.org](https://golang.testcontainers.org).
2 changes: 1 addition & 1 deletion docs/contributing.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Contributing

We follow the same guidelines used by testcontainers-java - please [check them
We follow the same guidelines used by testcontainers-java -- please [check them
out](https://www.testcontainers.org/contributing/).
144 changes: 144 additions & 0 deletions docs/examples/cockroachdb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# CockroachDB

```go
package main

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

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
_ "github.com/jackc/pgx/v4/stdlib"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

// Task represents a unit of work to complete. We're going to be using this in
// our example as a way to organize data that is being manipulated in
// the database.
type task struct {
ID string `json:"id"`
Description string `json:"description"`
DateDue *time.Time `json:"date_due,string"`
DateCreated time.Time `json:"date_created,string"`
DateUpdated time.Time `json:"date_updated"`
}

type cockroachDBContainer struct {
testcontainers.Container
URI string
}

func setupCockroachDB(ctx context.Context) (*cockroachDBContainer, error) {
req := testcontainers.ContainerRequest{
Image: "cockroachdb/cockroach:latest-v21.1",
ExposedPorts: []string{"26257/tcp", "8080/tcp"},
WaitingFor: wait.ForHTTP("/health").WithPort("8080"),
Cmd: []string{"start-single-node", "--insecure"},
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, err
}

mappedPort, err := container.MappedPort(ctx, "26257")
if err != nil {
return nil, err
}

hostIP, err := container.Host(ctx)
if err != nil {
return nil, err
}

uri := fmt.Sprintf("postgres://root@%s:%s", hostIP, mappedPort.Port())

return &cockroachDBContainer{Container: container, URI: uri}, nil
}

func initCockroachDB(ctx context.Context, db sql.DB) error {
// Actual SQL for initializing the database should probably live elsewhere
const query = `CREATE DATABASE projectmanagement;
CREATE TABLE projectmanagement.task(
id uuid primary key not null,
description varchar(255) not null,
date_due timestamp with time zone,
date_created timestamp with time zone not null,
date_updated timestamp with time zone not null);`
_, err := db.ExecContext(ctx, query)

return err
}

func truncateCockroachDB(ctx context.Context, db sql.DB) error {
const query = `TRUNCATE projectmanagement.task`
_, err := db.ExecContext(ctx, query)
return err
}

func TestIntegrationDBInsertSelect(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}

ctx := context.Background()

cdbContainer, err := setupCockroachDB(ctx)
if err != nil {
t.Fatal(err)
}
defer cdbContainer.Terminate(ctx)

db, err := sql.Open("pgx", cdbContainer.URI+"/projectmanagement")
if err != nil {
t.Fatal(err)
}
defer db.Close()

err = initCockroachDB(ctx, *db)
if err != nil {
t.Fatal(err)
}
defer truncateCockroachDB(ctx, *db)

now := time.Now()

// Insert data
tsk := task{ID: uuid.NewString(), Description: "Update resumé", DateCreated: now, DateUpdated: now}
const insertQuery = `insert into "task" (id, description, date_due, date_created, date_updated)
values ($1, $2, $3, $4, $5)`
_, err = db.ExecContext(
ctx,
insertQuery,
tsk.ID,
tsk.Description,
tsk.DateDue,
tsk.DateCreated,
tsk.DateUpdated)
if err != nil {
t.Fatal(err)
}

// Select data
savedTsk := task{ID: tsk.ID}
const findQuery = `select description, date_due, date_created, date_updated
from task
where id = $1`
row := db.QueryRowContext(ctx, findQuery, tsk.ID)
err = row.Scan(&savedTsk.Description, &savedTsk.DateDue, &savedTsk.DateCreated, &savedTsk.DateUpdated)
if err != nil {
t.Fatal(err)
}

if !cmp.Equal(tsk, savedTsk) {
t.Fatalf("Saved task is not the same:\n%s", cmp.Diff(tsk, savedTsk))
}
}
```
70 changes: 70 additions & 0 deletions docs/examples/nginx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# NGINX

```go
package main

import (
"context"
"fmt"
"net/http"
"testing"

"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)

type nginxContainer struct {
testcontainers.Container
URI string
}

func setupNginx(ctx context.Context) (*nginxContainer, error) {
req := testcontainers.ContainerRequest{
Image: "nginx",
ExposedPorts: []string{"80/tcp"},
WaitingFor: wait.ForHTTP("/"),
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, err
}

ip, err := container.Host(ctx)
if err != nil {
return nil, err
}

mappedPort, err := container.MappedPort(ctx, "80")
if err != nil {
return nil, err
}

uri := fmt.Sprintf("http://%s:%s", ip, mappedPort.Port())

return &nginxContainer{Container: container, URI: uri}, nil
}

func TestIntegrationNginxLatestReturn(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}

ctx := context.Background()

nginxC, err := setupNginx(ctx)
if err != nil {
t.Fatal(err)
}

// Clean up the container after the test is complete
defer nginxC.Terminate(ctx)

resp, err := http.Get(nginxC.URI)
if resp.StatusCode != http.StatusOK {
t.Fatalf("Expected status code %d. Got %d.", http.StatusOK, resp.StatusCode)
}
}
```
Loading