Starting to implement integration tests

main
Drew Bednar 4 days ago
parent 792ad0f10c
commit 7a38cb536a

@ -19,7 +19,7 @@ test-short:
.PHONY: test-short
test-int:
go test $(FLAGS) -count=1 ./cmd/api/handlers_integration_test.go -integration-handlers
go test $(FLAGS) -count=1 -tags=integration ./cmd/api/...
.PHONY: test-int
## Coverage See also -covermode=count and -covermode=atomic

@ -17,6 +17,23 @@ make start-local
air
```
## Testing
Unit tests:
```bash
make test
```
Integration tests. Assumes `start-local` has been run and ready or `PULLEY_INTEGRATION_DATABASE_URI` is specified.
```bash
make test-int
```
## Routes

@ -1,18 +1,57 @@
//go:build integration
package main
import (
"flag"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"git.runcible.io/learning/pulley/internal/assert"
"git.runcible.io/learning/pulley/internal/data"
"git.runcible.io/learning/pulley/internal/testutil"
)
var runIntegrationTestHandlers bool
func TestHttpListHandlers(t *testing.T) {
pool := testutil.SetupTestDB(t)
func init() {
flag.BoolVar(&runIntegrationTestHandlers, "integration-handlers", false, "run integration tests for http handlers")
movies := []data.Movie{
{
ID: 1337,
Title: "Immortals",
Runtime: data.Runtime(110),
Genres: []string{"action", "adventure"},
Year: 2011,
Version: 1,
},
}
func TestHttpListHandlers(t *testing.T) {
if !runIntegrationTestHandlers {
t.Skip("Skipping handler integration tests")
}
testutil.SeedMovies(t, pool, movies)
app := newTestApplication(pool)
t.Run("test list movies", func(t *testing.T) {
rec := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/v1/movies", nil)
assert.NilError(t, err)
app.routes().ServeHTTP(rec, r)
resp := rec.Result()
assert.Equal(t, resp.StatusCode, http.StatusOK)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
assert.NilError(t, err)
content := make(map[string][]data.Movie)
err = json.Unmarshal(body, &content)
assert.NilError(t, err)
assert.Equal(t, len(content["movies"]), 1)
assert.MovieEqual(t, content["movies"][0], movies[0])
})
}

@ -1,3 +1,5 @@
//go:build !integration
package main
import (
@ -5,35 +7,20 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"git.runcible.io/learning/pulley/internal/assert"
"git.runcible.io/learning/pulley/internal/config"
"git.runcible.io/learning/pulley/internal/data"
"git.runcible.io/learning/pulley/internal/database"
"github.com/pashagolub/pgxmock/v4"
)
var defaultFixedTime = time.Date(2026, 1, 25, 10, 10, 40, 0, time.UTC)
var getAllReturnColumns = []string{"id", "created_at", "title", "year", "runtime", "genres", "version"}
func newTestApplication(pool database.PgxIface) application {
cfg := config.ServiceConfig{Env: "test"}
mockModels := data.NewModels(pool)
// Discards log output from tests
// logger := slog.New(slog.NewTextHandler(io.Discard, nil))
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}))
return application{config: cfg, logger: logger, models: mockModels}
}
func TestHealthRoute(t *testing.T) {
respRec := httptest.NewRecorder()
mockPool, err := pgxmock.NewPool()

@ -0,0 +1,21 @@
package main
import (
"log/slog"
"os"
"git.runcible.io/learning/pulley/internal/config"
"git.runcible.io/learning/pulley/internal/data"
"git.runcible.io/learning/pulley/internal/database"
)
func newTestApplication(pool database.PgxIface) application {
cfg := config.ServiceConfig{Env: "test"}
mockModels := data.NewModels(pool)
// Discards log output from tests
// logger := slog.New(slog.NewTextHandler(io.Discard, nil))
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug, AddSource: true}))
return application{config: cfg, logger: logger, models: mockModels}
}

@ -10,8 +10,10 @@ import (
"time"
"git.runcible.io/learning/pulley/internal/config"
"git.runcible.io/learning/pulley/internal/data"
"git.runcible.io/learning/pulley/internal/database"
"git.runcible.io/learning/pulley/migrations"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
@ -109,10 +111,14 @@ func SetupTestDB(t *testing.T) *pgxpool.Pool {
t.Cleanup(func() {
t.Logf("Ensuring test pool is closed")
testPool.Close()
// TODO envvar check could be beneficial if DB needs to be inspected
// because of failure
t.Logf("Cleaning up database %s", testDbName)
if t.Failed() && os.Getenv("PULLEY_PRESERVE_FAILED_DB") == "true" {
t.Logf("Test failed; skipping db cleanup for %s", testDbName)
return
}
ctxCleanup, cleanupCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cleanupCancel()
@ -126,3 +132,35 @@ func SetupTestDB(t *testing.T) *pgxpool.Pool {
return testPool
}
// SeedMovie inserts movies into the movies database table
func SeedMovies(t *testing.T, pool *pgxpool.Pool, movies []data.Movie) {
t.Helper()
ctxTimeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tx, err := pool.BeginTx(ctxTimeout, pgx.TxOptions{})
if err != nil {
t.Fatalf("Failed to begin seed transaction: %v", err)
}
defer tx.Rollback(ctxTimeout)
query := `
INSERT INTO movies (id, title, year, runtime, genres, version)
VALUES ($1, $2, $3, $4, $5, $6)`
for _, m := range movies {
_, err := tx.Exec(ctxTimeout, query, m.ID, m.Title, m.Year, m.Runtime, m.Genres, m.Version)
if err != nil {
t.Fatalf("failed to insert movie %q: %v", m.Title, err)
}
}
if err = tx.Commit(ctxTimeout); err != nil {
t.Fatalf("Failed to commit seed transaction: %v", err)
}
}

Loading…
Cancel
Save