From dee4b419caed3872527ce8d2d38805d8cc18e3a4 Mon Sep 17 00:00:00 2001 From: Paul Balogh Date: Wed, 24 Jan 2024 06:10:32 -0600 Subject: [PATCH] Provide examples of varied testcontainer implementation styles Examples created for StLGo Meetup (https://www.youtube.com/watch?v=sX4s1HqPZcw) Signed-off-by: Paul Balogh --- db/place_sequential_test.go | 112 ++++++++++++++++++++++++++++ db/place_suite_test.go | 145 ++++++++++++++++++++++++++++++++++++ db/place_test.go | 5 ++ 3 files changed, 262 insertions(+) create mode 100644 db/place_sequential_test.go create mode 100644 db/place_suite_test.go diff --git a/db/place_sequential_test.go b/db/place_sequential_test.go new file mode 100644 index 0000000..e561307 --- /dev/null +++ b/db/place_sequential_test.go @@ -0,0 +1,112 @@ +package db + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/weesvc/weesvc-gorilla/model" + + "github.com/stretchr/testify/assert" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" +) + +// Here we're testing each CRUD method sequentially using a single container for the test. +// This is a bit more of a brute force test, but fastest to "market." + +// TestDatabase provided as an example where we do ALL THE THINGS in a single testcase. +func TestDatabase(t *testing.T) { + // _Real_ test is provided with `place_test.go`...this is just for learning. + t.Skip() + + t.Parallel() + ctx := context.Background() + + pgContainer, err := postgres.RunContainer(ctx, + testcontainers.WithImage("postgres:15.3-alpine"), + postgres.WithInitScripts(filepath.Join("..", "testdata", "init-db.sql")), + postgres.WithDatabase("test-db"), + postgres.WithUsername("postgres"), + postgres.WithPassword("postgres"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2).WithStartupTimeout(5*time.Second)), + ) + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + if terr := pgContainer.Terminate(ctx); terr != nil { + t.Fatalf("failed to terminate pgContainer: %s", terr) + } + }) + + connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") + if err != nil { + t.Fatal(err) + } + + placeDB, err := New(&Config{ + DatabaseURI: connStr, + Dialect: "postgres", + Verbose: true, + }) + if err != nil { + t.Fatal(err) + } + + // Check 1: Retrieve all places + places, err := placeDB.GetPlaces() + if err != nil { + t.Fatal(err) + } + assert.Equal(t, len(places), 10) + + // Check 2: Add a new place + newPlace := &model.Place{ + ID: 20, + Name: "Kerid Crater", + Description: "Kerid Crater, Iceland", + Latitude: 64.04126, + Longitude: -20.88530, + } + err = placeDB.CreatePlace(newPlace) + if err != nil { + t.Fatal(err) + } + + // Check 3: Retrieve the new place + place, err := placeDB.GetPlaceByID(newPlace.ID) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, place.ID, newPlace.ID) + assert.Equal(t, place.Name, newPlace.Name) + assert.Equal(t, place.Description, newPlace.Description) + assert.Equal(t, place.Latitude, newPlace.Latitude) + assert.Equal(t, place.Longitude, newPlace.Longitude) + assert.NotNil(t, place.CreatedAt) + assert.NotNil(t, place.UpdatedAt) + + // Check 4: Update a place + updatedDescription := "UPDATED" + err = placeDB.UpdatePlace(&model.Place{ID: newPlace.ID, Description: updatedDescription}) + if err != nil { + t.Fatal(err) + } + place, _ = placeDB.GetPlaceByID(newPlace.ID) + assert.Equal(t, updatedDescription, place.Description) + assert.Greater(t, place.UpdatedAt, place.CreatedAt) + + // Check 5: Delete the place + err = placeDB.DeletePlaceByID(newPlace.ID) + if err != nil { + t.Fatal(err) + } + _, err = placeDB.GetPlaceByID(newPlace.ID) + assert.EqualError(t, err, "unable to get place: record not found") +} diff --git a/db/place_suite_test.go b/db/place_suite_test.go new file mode 100644 index 0000000..23a31b8 --- /dev/null +++ b/db/place_suite_test.go @@ -0,0 +1,145 @@ +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. + +// TestDatabase_TestSuite executes the suite of tests. +func TestDatabase_TestSuite(t *testing.T) { + // _Real_ test is provided with `place_test.go`...this is just for learning. + t.Skip() + + t.Parallel() + suite.Run(t, new(PlaceTestSuite)) +} + +// 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.NotEqual(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") + } + } +} diff --git a/db/place_test.go b/db/place_test.go index 6dec857..416ced0 100644 --- a/db/place_test.go +++ b/db/place_test.go @@ -11,6 +11,11 @@ import ( "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)