Skip to content

Commit

Permalink
chore: add REST API tests
Browse files Browse the repository at this point in the history
  • Loading branch information
antosdaniel committed Sep 5, 2023
1 parent a1479f6 commit 2fe50e1
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 38 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* text=auto
2 changes: 1 addition & 1 deletion app_to_test/expenses.http
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Content-Type: application/json
"id": "{{$random.uuid}}",
"amount": {{$random.integer}},
"category": "Shopping",
"date": "2023-08-10",
"date": "2023-08-10T00:00:00Z",
"notes": "Shopping spree"
}

Expand Down
8 changes: 8 additions & 0 deletions app_to_test/server/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ func addExpenseHandler(expenseRepo *ExpenseRepo) http.HandlerFunc {
var expense Expense
err := json.NewDecoder(r.Body).Decode(&expense)
if err != nil {
slog.With(slog.String("error", err.Error())).Warn("invalid request body")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "invalid request body"}`))
return
}

if expense.Category == "" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "category is required"}`))
return
}

Expand Down
5 changes: 4 additions & 1 deletion app_to_test/server/api/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ func (r *ExpenseRepo) Add(expense Expense) error {
}

func (r *ExpenseRepo) All() ([]Expense, error) {
rows, err := r.db.Query("SELECT id, amount, category, date, notes FROM expenses")
rows, err := r.db.Query(`
SELECT id, amount, category, date, notes
FROM expenses
`)
if err != nil {
return nil, err
}
Expand Down
41 changes: 5 additions & 36 deletions test_repos/repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@ package test_repos
import (
"context"
"database/sql"
"fmt"
"testing"
"time"

"github.com/antosdaniel/go-presentation-beyond-unit-tests/app_to_test/server/api"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
tc "github.com/testcontainers/testcontainers-go/modules/compose"
"github.com/testcontainers/testcontainers-go/wait"
)

// This would usually live next to repo it is testing
Expand All @@ -21,7 +18,7 @@ func TestExpenseRepo_Add(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()

db := startDB(t, ctx)
db := StartDB(t, ctx).DB
expenseRepo := api.NewExpenseRepo(db)

t.Run("successfully adds expense", func(t *testing.T) {
Expand Down Expand Up @@ -59,42 +56,14 @@ func TestExpenseRepo_Add(t *testing.T) {
})
}

// This should be reused by other repo tests. It is kept here for simplicity.

func startDB(t *testing.T, ctx context.Context) *sql.DB {
t.Helper()

compose, err := tc.NewDockerCompose("./../docker-compose-db-only.yaml")
require.NoError(t, err, "docker compose setup")

t.Cleanup(func() {
compose.Down(context.Background(), tc.RemoveOrphans(true), tc.RemoveImagesLocal)
})

err = compose.WaitForService("migrate", wait.ForExit()).Up(ctx)
require.NoError(t, err, "docker compose up")

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

dbPort, err := dbContainer.MappedPort(ctx, "5432")
require.NoError(t, err, "docker compose db port")

dsn := fmt.Sprintf("postgres://postgres:secret123@localhost:%s/expense_tracker?sslmode=disable", dbPort.Port())
db, err := sql.Open("pgx", dsn)
require.NoError(t, err, "pgx open")
t.Cleanup(func() {
db.Close()
})

return db
}

// getAllExpenses Test helper for getting all expenses from the database.
func getAllExpenses(t *testing.T, db *sql.DB) api.Expenses {
t.Helper()

row, err := db.Query("SELECT id, amount, category, date, notes FROM expenses")
row, err := db.Query(`
SELECT id, amount, category, date, notes
FROM expenses
`)
if err != nil {
t.Fatalf("could not query expenses: %v", err)
}
Expand Down
49 changes: 49 additions & 0 deletions test_repos/start_db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package test_repos

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

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

type DBContainer struct {
DB *sql.DB
DSN string
}

func StartDB(t *testing.T, ctx context.Context) DBContainer {
t.Helper()

compose, err := tc.NewDockerCompose("./../docker-compose-db-only.yaml")
require.NoError(t, err, "docker compose setup")

t.Cleanup(func() {
compose.Down(context.Background(), tc.RemoveOrphans(true), tc.RemoveImagesLocal)
})

err = compose.WaitForService("migrate", wait.ForExit()).Up(ctx)
require.NoError(t, err, "docker compose up")

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

dbPort, err := dbContainer.MappedPort(ctx, "5432")
require.NoError(t, err, "docker compose db port")

dsn := fmt.Sprintf("postgres://postgres:secret123@localhost:%s/expense_tracker?sslmode=disable", dbPort.Port())
db, err := sql.Open("pgx", dsn)
require.NoError(t, err, "pgx open")
t.Cleanup(func() {
db.Close()
})

return DBContainer{
DB: db,
DSN: dsn,
}
}
106 changes: 106 additions & 0 deletions test_rest_api/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package test_rest_api

import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"

"github.com/antosdaniel/go-presentation-beyond-unit-tests/app_to_test/server/api"
"github.com/antosdaniel/go-presentation-beyond-unit-tests/test_repos"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAPI(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()

server := startServer(t, ctx)

t.Run("summarize expenses", func(t *testing.T) {
// Notice that if we moved this test after add expense test, this test would fail.
// Usually it's better to reset the database before each test.
response, responseBody := call(t, server, http.MethodGet, "/expenses/summarize", "")

assert.Equal(t, http.StatusOK, response.StatusCode, "status code")
expected := getExpectedResponse(t)
assert.JSONEq(t, expected, responseBody, "response body")
})
t.Run("add expense successfully", func(t *testing.T) {
response, _ := call(t, server, http.MethodPost, "/expenses/add", getRequest(t))

assert.Equal(t, http.StatusCreated, response.StatusCode, "status code")
// We should check if expense was added to the database here.
})
t.Run("add expense fails", func(t *testing.T) {
response, responseBody := call(t, server, http.MethodPost, "/expenses/add", getRequest(t))

assert.Equal(t, http.StatusBadRequest, response.StatusCode, "status code")
expected := getExpectedResponse(t)
assert.JSONEq(t, expected, responseBody, "response body")
})
}

func startServer(t *testing.T, ctx context.Context) *httptest.Server {
t.Helper()

dbContainer := test_repos.StartDB(t, ctx)
require.NoError(t, os.Setenv("DB_URL", dbContainer.DSN), "set DB_URL env var")

setup, err := api.NewSetup()
require.NoError(t, err, "new setup")

server := httptest.NewServer(setup.APIMux)
t.Cleanup(func() {
server.Close()
})

return server
}

func call(t *testing.T, server *httptest.Server, method, path, body string) (*http.Response, string) {
t.Helper()

var b io.Reader
if body != "" {
b = bytes.NewBuffer([]byte(body))
}
request, err := http.NewRequest(method, server.URL+path, b)
require.NoError(t, err, "new request")

response, err := server.Client().Do(request)
require.NoError(t, err, "do request")

responseBody, err := io.ReadAll(response.Body)
require.NoError(t, err, "read response body")

return response, string(responseBody)

}

func getRequest(t *testing.T) string {
t.Helper()

path := fmt.Sprintf("./testdata/%s/request.json", t.Name())
file, err := os.ReadFile(path)
require.NoError(t, err, "read file")

return string(file)
}

func getExpectedResponse(t *testing.T) string {
t.Helper()

path := fmt.Sprintf("./testdata/%s/response.json", t.Name())
file, err := os.ReadFile(path)
require.NoError(t, err, "read file")

return string(file)
}
7 changes: 7 additions & 0 deletions test_rest_api/testdata/TestAPI/add_expense_fails/request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "057854f2-ca02-4ee7-92b0-d029125575d8",
"amount": 2500,
"category": "",
"date": "2023-08-10T00:00:00Z",
"notes": "Shopping spree"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"error": "category is required"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "057854f2-ca02-4ee7-92b0-d029125575d8",
"amount": 2500,
"category": "Shopping",
"date": "2023-08-10T00:00:00Z",
"notes": "Shopping spree"
}
32 changes: 32 additions & 0 deletions test_rest_api/testdata/TestAPI/summarize_expenses/response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"year": 2023,
"month": 6,
"category": "Dining",
"total_amount": 130
},
{
"year": 2023,
"month": 6,
"category": "Entertainment",
"total_amount": 55
},
{
"year": 2023,
"month": 6,
"category": "Groceries",
"total_amount": 285
},
{
"year": 2023,
"month": 6,
"category": "Shopping",
"total_amount": 120
},
{
"year": 2023,
"month": 6,
"category": "Transportation",
"total_amount": 60
}
]

0 comments on commit 2fe50e1

Please sign in to comment.