Skip to content

Commit

Permalink
Incorporate use of Testcontainers for unit testing
Browse files Browse the repository at this point in the history
Includes multiple examples for use in meetup talk, https://www.meetup.com/stl-go/events/298426035/

Signed-off-by: Paul Balogh <[email protected]>
  • Loading branch information
javaducky committed Jan 19, 2024
1 parent 2f4281f commit 23ba942
Show file tree
Hide file tree
Showing 6 changed files with 562 additions and 7 deletions.
142 changes: 142 additions & 0 deletions db/place_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package db

import (
"context"
"log"
"testing"

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

"github.com/weesvc/weesvc-gorilla/model"
"github.com/weesvc/weesvc-gorilla/testhelpers"
)

// Here we're testing each CRUD method independently, but we're using a single container
// for the entire test suite. This _may_ be ok, but you need to be wary of what is changing
// in the database as suite tests may collide.
//
// With this method, we've refactored some container work into the `testhelpers` package.

// PlaceTestSuite contains shared state amongst all test(s) within the suite.
type PlaceTestSuite struct {
suite.Suite
pgContainer *testhelpers.PostgresContainer
ctx context.Context
placeDb *Database // Reference to our test fixture
}

// SetupSuite executes prior to any test(s) in order to prepare the shared state.
func (suite *PlaceTestSuite) SetupSuite() {
suite.ctx = context.Background()
pgContainer, err := testhelpers.CreatePostgresContainer(suite.ctx)
if err != nil {
log.Fatal(err)
}
suite.pgContainer = pgContainer
placeDb, err := New(&Config{
DatabaseURI: pgContainer.ConnectionString,
Dialect: "postgres",
Verbose: true,
})
if err != nil {
log.Fatal(err)
}
suite.placeDb = placeDb
}

// TearDownSuite executes cleanup after all test(s) have run.
func (suite *PlaceTestSuite) TearDownSuite() {
if err := suite.pgContainer.Terminate(suite.ctx); err != nil {
log.Fatalf("error terminating postgres container: %s", err)
}
}

func (suite *PlaceTestSuite) Test_GetPlaces() {
places, err := suite.placeDb.GetPlaces()
if assert.NoError(suite.T(), err) {
assert.Greater(suite.T(), len(places), 0)
}
}

func (suite *PlaceTestSuite) Test_GetPlaceByID() {
fetchId := uint(6)
place, err := suite.placeDb.GetPlaceByID(fetchId)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), fetchId, place.ID)
assert.Equal(suite.T(), "MIA", place.Name)
assert.Equal(suite.T(), "Miami International Airport, FL, USA", place.Description)
assert.Equal(suite.T(), 25.79516, place.Latitude)
assert.Equal(suite.T(), -80.27959, place.Longitude)
assert.NotNil(suite.T(), place.CreatedAt)
assert.NotNil(suite.T(), place.UpdatedAt)
}
}

func (suite *PlaceTestSuite) Test_CreatePlace() {
newPlace := &model.Place{
ID: 20,
Name: "Kerid Crater",
Description: "Kerid Crater, Iceland",
Latitude: 64.04126,
Longitude: -20.88530,
}
err := suite.placeDb.CreatePlace(newPlace)
if assert.NoError(suite.T(), err) {
// Verify our inserted newPlace
created, err := suite.placeDb.GetPlaceByID(newPlace.ID)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), newPlace.ID, created.ID)
assert.Equal(suite.T(), newPlace.Name, created.Name)
assert.Equal(suite.T(), newPlace.Description, created.Description)
assert.Equal(suite.T(), newPlace.Latitude, created.Latitude)
assert.Equal(suite.T(), newPlace.Longitude, created.Longitude)
assert.NotNil(suite.T(), created.CreatedAt)
assert.NotNil(suite.T(), created.UpdatedAt)
}
}
}

func (suite *PlaceTestSuite) Test_UpdatePlace() {
original, err := suite.placeDb.GetPlaceByID(7)
if assert.NoError(suite.T(), err) {
changes := &model.Place{
ID: original.ID,
Name: "The Alamo",
Description: "The Alamo, San Antonio, TX, USA",
Latitude: 29.42590,
Longitude: -98.48625,
}
if assert.NoError(suite.T(), suite.placeDb.UpdatePlace(changes)) {
// Verify the updated place
updated, err := suite.placeDb.GetPlaceByID(original.ID)
if assert.NoError(suite.T(), err) {
assert.Equal(suite.T(), original.ID, updated.ID)
assert.Equal(suite.T(), changes.Name, updated.Name)
assert.Equal(suite.T(), changes.Description, updated.Description)
assert.Equal(suite.T(), changes.Latitude, updated.Latitude)
assert.Equal(suite.T(), changes.Longitude, updated.Longitude)
assert.Equal(suite.T(), original.CreatedAt, updated.CreatedAt)
assert.Greater(suite.T(), original.UpdatedAt, updated.UpdatedAt)
}
}
}
}

func (suite *PlaceTestSuite) Test_DeletePlaceByID() {
deleteID := uint(1)
_, err := suite.placeDb.GetPlaceByID(deleteID)
if assert.NoError(suite.T(), err) {
if assert.NoError(suite.T(), suite.placeDb.DeletePlaceByID(deleteID)) {
// Verify no longer retrievable
_, err = suite.placeDb.GetPlaceByID(deleteID)
assert.EqualError(suite.T(), err, "unable to get place: record not found")
}
}
}

// TestDatabase_TestSuite executes the suite of tests.
func TestDatabase_TestSuite(t *testing.T) {
t.Parallel()
suite.Run(t, new(PlaceTestSuite))
}
140 changes: 140 additions & 0 deletions db/place_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package db

import (
"context"
"log"
"testing"

"github.com/weesvc/weesvc-gorilla/model"
"github.com/weesvc/weesvc-gorilla/testhelpers"

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

// Here we're testing each CRUD method independently, but we're using a fresh container
// for each test. This _may_ be ok, but could be a bit of overhead for CICD pipelines.
//
// With this method, we've refactored some container work into the `testhelpers` package.

func TestDatabase_GetPlaces(t *testing.T) {
t.Parallel()
placeDb := setupDatabase(t)

places, err := placeDb.GetPlaces()
assert.NoError(t, err)
assert.Equal(t, 10, len(places))
}

func TestDatabase_GetPlaceByID(t *testing.T) {
t.Parallel()
placeDb := setupDatabase(t)

fetchId := uint(6)
place, err := placeDb.GetPlaceByID(fetchId)
if assert.NoError(t, err) {
assert.Equal(t, fetchId, place.ID)
assert.Equal(t, "MIA", place.Name)
assert.Equal(t, "Miami International Airport, FL, USA", place.Description)
assert.Equal(t, 25.79516, place.Latitude)
assert.Equal(t, -80.27959, place.Longitude)
assert.NotNil(t, place.CreatedAt)
assert.NotNil(t, place.UpdatedAt)
}
}

func TestDatabase_CreatePlace(t *testing.T) {
t.Parallel()
placeDb := setupDatabase(t)

newPlace := &model.Place{
ID: 20,
Name: "Kerid Crater",
Description: "Kerid Crater, Iceland",
Latitude: 64.04126,
Longitude: -20.88530,
}
err := placeDb.CreatePlace(newPlace)
if assert.NoError(t, err) {
// Verify our inserted place
created, err := placeDb.GetPlaceByID(newPlace.ID)
if assert.NoError(t, err) {
assert.Equal(t, newPlace.ID, created.ID)
assert.Equal(t, newPlace.Name, created.Name)
assert.Equal(t, newPlace.Description, created.Description)
assert.Equal(t, newPlace.Latitude, created.Latitude)
assert.Equal(t, newPlace.Longitude, created.Longitude)
assert.NotNil(t, created.CreatedAt)
assert.NotNil(t, created.UpdatedAt)
}
}
}

func TestDatabase_UpdatePlace(t *testing.T) {
t.Parallel()
placeDb := setupDatabase(t)

original, err := placeDb.GetPlaceByID(7)
if assert.NoError(t, err) {
changes := &model.Place{
ID: original.ID,
Name: "The Alamo",
Description: "The Alamo, San Antonio, TX, USA",
Latitude: 29.42590,
Longitude: -98.48625,
}
if assert.NoError(t, placeDb.UpdatePlace(changes)) {
// Verify the updated place
updated, err := placeDb.GetPlaceByID(original.ID)
if assert.NoError(t, err) {
assert.Equal(t, original.ID, updated.ID)
assert.Equal(t, changes.Name, updated.Name)
assert.Equal(t, changes.Description, updated.Description)
assert.Equal(t, changes.Latitude, updated.Latitude)
assert.Equal(t, changes.Longitude, updated.Longitude)
assert.Equal(t, original.CreatedAt, updated.CreatedAt)
assert.Greater(t, original.UpdatedAt, updated.UpdatedAt)
}
}
}
}

func TestDatabase_DeletePlaceByID(t *testing.T) {
t.Parallel()
placeDb := setupDatabase(t)

deleteID := uint(1)
_, err := placeDb.GetPlaceByID(deleteID)
if assert.NoError(t, err) {
if assert.NoError(t, placeDb.DeletePlaceByID(deleteID)) {
// Verify no longer retrievable
_, err = placeDb.GetPlaceByID(deleteID)
assert.EqualError(t, err, "unable to get place: record not found")
}
}
}

func setupDatabase(t *testing.T) *Database {
ctx := context.Background()

pgContainer, err := testhelpers.CreatePostgresContainer(ctx)
if err != nil {
log.Fatal(err)
}

placeDb, err := New(&Config{
DatabaseURI: pgContainer.ConnectionString,
Dialect: "postgres",
Verbose: true,
})
if err != nil {
log.Fatal(err)
}

t.Cleanup(func() {
if err := pgContainer.Terminate(ctx); err != nil {
t.Fatalf("failed to terminate pgContainer: %s", err)
}
})

return placeDb
}
43 changes: 42 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,71 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4
github.com/testcontainers/testcontainers-go v0.27.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.27.0
)

require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/containerd v1.7.11 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.7+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/lib/pq v1.1.1 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.11 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading

0 comments on commit 23ba942

Please sign in to comment.