Skip to content

Commit

Permalink
chore: add e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
antosdaniel committed Sep 10, 2023
1 parent 503af3b commit 4d355a5
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 0 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ Next one is [test_http_api](test_http_api). It includes database trick from
previous suite, but also spins up API. Explored idea here is how to keep tests
like these easy to read, and not a chore to write (at least, after the first one).

Final one is [test_e2e](test_e2e). We are spinning up whole application, and
running tests against that. To make things even more interesting, we are stubing
external API for better reliability. This checks that our application starts correctly,
and that all the components work together. It's certainly more complex, not as
quick to write, nor run. Use them sparingly, to make sure that crucial parts
of your application work as expected. These could also be named smoke tests.

## Requirements

- Go 1.21
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
version: "3.9"
services:
server:
image: "expense_tracker/server:latest"
build:
dockerfile: app_to_test/server/Dockerfile
environment:
Expand Down Expand Up @@ -32,6 +33,7 @@ services:
- "5432:5432"

migrate:
image: "expense_tracker/migrate:latest"
build:
context: app_to_test/db
environment:
Expand All @@ -41,6 +43,7 @@ services:
condition: service_healthy

bank-api:
image: "expense_tracker/bank_api:latest"
build:
context: ./bank_api
networks:
Expand Down
44 changes: 44 additions & 0 deletions test_e2e/docker-compose-for-e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
version: "3.9"
services:
server:
image: "expense_tracker/server:latest"
build:
context: ./..
dockerfile: app_to_test/server/Dockerfile
environment:
DB_URL: "postgres://postgres:secret123@db:5432/expense_tracker?sslmode=disable"
BANK_API_URL: "${BANK_API_URL}"
depends_on:
db:
condition: service_healthy
networks:
- default
ports:
# Once again, we use a random port to avoid conflicts.
- "8000"

db:
image: "postgres:15.2-alpine"
environment:
POSTGRES_DB: expense_tracker
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret123
healthcheck:
test: [ "CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}" ]
interval: 3s
timeout: 60s
retries: 10
start_period: 5s

migrate:
image: "expense_tracker/migrate:latest"
build:
context: ./../app_to_test/db
environment:
DB_URL: "postgres://postgres:secret123@db:5432/expense_tracker?sslmode=disable"
depends_on:
db:
condition: service_healthy

networks:
default:
131 changes: 131 additions & 0 deletions test_e2e/e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//go:build e2e_tests

package test_e2e

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

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
tc "github.com/testcontainers/testcontainers-go/modules/compose"
"github.com/testcontainers/testcontainers-go/wait"
)

const expenseToSyncID = "677df0c4-d829-42eb-a0c9-29d5b0a2bbe4"

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

// At first, spin up mocked bank API, and get its address.
bankAPIAddress := mockBankAPI(t)
address := startApp(t, ctx, bankAPIAddress)

t.Run("app is starting properly", func(t *testing.T) {
response, err := http.Get(fmt.Sprintf("%s/expenses/all", address))

require.NoError(t, err)
assert.Equal(t, http.StatusOK, response.StatusCode, "status code")
})
t.Run("sync expenses", func(t *testing.T) {
response, err := http.Get(fmt.Sprintf("%s/expenses/sync", address))

require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode, "status code")

// After sync, our app should now store expense from mocked API.
response, err = http.Get(fmt.Sprintf("%s/expenses/all", address))
require.NoError(t, err)
responseBody, err := io.ReadAll(response.Body)
require.NoError(t, err)
assert.Contains(t, string(responseBody), expenseToSyncID)
})
}

func startApp(t *testing.T, ctx context.Context, bankAPIAddress string) (address string) {
t.Helper()

compose, err := tc.NewDockerComposeWith(
tc.WithStackFiles("./docker-compose-for-e2e.yaml"),
// Giving unique name to each docker compose stack allows us to run tests in parallel.
tc.StackIdentifier(uuid.New().String()),
)
require.NoError(t, err, "docker compose setup")

t.Cleanup(func() {
// When test fail, printing logs is usually helpful :)
if t.Failed() {
reader, _ := getServerContainer(t, ctx, compose).Logs(ctx)
bytes, _ := io.ReadAll(reader)
fmt.Println(`\nLogs from "server" container:\n`, string(bytes))
}
assert.NoError(t, compose.Down(ctx, tc.RemoveOrphans(true), tc.RemoveImagesLocal))
})

err = compose.
WithEnv(map[string]string{
"BANK_API_URL": bankAPIAddress,
}).
WaitForService("server", wait.ForLog("running...")).
Up(ctx)
require.NoError(t, err, "docker compose up")

// Port is randomly assigned by docker. We need to get it.
apiPort, err := getServerContainer(t, ctx, compose).MappedPort(ctx, "8000")
require.NoError(t, err, "docker compose server port")

return fmt.Sprintf("http://localhost:%s", apiPort.Port())
}

func getServerContainer(t *testing.T, ctx context.Context, compose tc.ComposeStack) testcontainers.Container {
t.Helper()

serverContainer, err := compose.ServiceContainer(ctx, "server")
require.NoError(t, err, "docker compose server container")

return serverContainer
}

func mockBankAPI(t *testing.T) (address string) {
t.Helper()

// We are getting random available port.
// Using 0.0.0.0 address if preferable over localhost, as the former will usually not work in CI.
// Oh, and Desktop Docker doesn't support IPv6 yet, so it's better to specify "tcp4" network.
listener, err := net.Listen("tcp4", "0.0.0.0:0")
require.NoError(t, err, "could not start listener")
addr, err := net.ResolveTCPAddr(listener.Addr().Network(), listener.Addr().String())
require.NoError(t, err, "could not resolve tcp addr")

// Setup mocked API.
mux := http.NewServeMux()
mux.Handle("/get-transactions", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf(`
[
{
"id": "%s",
"amount": 500.00,
"category": "food",
"created_at": "2020-01-01T00:00:00Z"
}
]`,
expenseToSyncID)))
}))

server := httptest.NewUnstartedServer(mux)
server.Listener = listener
server.Start()
t.Cleanup(func() {
server.Close()
})

// "host.docker.internal" resolves to host network. Thanks to this we can access our HTTP mocks from container.
return fmt.Sprintf("http://host.docker.internal:%d", addr.Port)
}
1 change: 1 addition & 0 deletions test_repos/docker-compose-db-only.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ services:
- "5432"

migrate:
image: "expense_tracker/migrate:latest"
build:
context: ../app_to_test/db
environment:
Expand Down

0 comments on commit 4d355a5

Please sign in to comment.