fix tests with logging and context

This commit is contained in:
Michael Thomson 2025-03-13 15:58:02 -04:00
parent 96975c7bd2
commit e76386d10a
9 changed files with 88 additions and 63 deletions

View File

@ -10,17 +10,17 @@ type ContextHandler struct {
slog.Handler slog.Handler
} }
func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error { func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
if requestID, ok := ctx.Value("trace_id").(string); ok { if requestID, ok := ctx.Value("trace_id").(string); ok {
r.AddAttrs(slog.String("trace_id", requestID)) r.AddAttrs(slog.String("trace_id", requestID))
} }
return h.Handler.Handle(ctx, r) return h.Handler.Handle(ctx, r)
} }
func NewLogger() *slog.Logger { func NewLogger() *slog.Logger {
baseHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: false}) baseHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: false})
customHandler := &ContextHandler{Handler: baseHandler} customHandler := &ContextHandler{Handler: baseHandler}
logger := slog.New(customHandler) logger := slog.New(customHandler)
return logger return logger
} }

View File

@ -2,7 +2,9 @@ package handler
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"log/slog"
"errors" "errors"
"net/http" "net/http"
@ -14,26 +16,27 @@ import (
) )
type MockTodoCreater struct { type MockTodoCreater struct {
CreateTodoFunc func(todo service.Todo) (service.Todo, error) CreateTodoFunc func(cxt context.Context, todo service.Todo) (service.Todo, error)
} }
func (tg *MockTodoCreater) CreateTodo(todo service.Todo) (service.Todo, error) { func (tg *MockTodoCreater) CreateTodo(ctx context.Context, todo service.Todo) (service.Todo, error) {
return tg.CreateTodoFunc(todo) return tg.CreateTodoFunc(ctx, todo)
} }
func TestCreateTodo(t *testing.T) { func TestCreateTodo(t *testing.T) {
logger := slog.Default()
t.Run("create todo", func(t *testing.T) { t.Run("create todo", func(t *testing.T) {
createTodoRequest := CreateTodoRequest{Name: "clean dishes", Done: false} createTodoRequest := CreateTodoRequest{Name: "clean dishes", Done: false}
createdTodo := service.Todo{Id: 1, Name: "clean dishes", Done: false} createdTodo := service.Todo{Id: 1, Name: "clean dishes", Done: false}
want := CreateTodoResponse{Id: 1, Name: "clean dishes", Done: false} want := CreateTodoResponse{Id: 1, Name: "clean dishes", Done: false}
service := MockTodoCreater{ service := MockTodoCreater{
CreateTodoFunc: func(todo service.Todo) (service.Todo, error) { CreateTodoFunc: func(ctx context.Context, todo service.Todo) (service.Todo, error) {
return createdTodo, nil return createdTodo, nil
}, },
} }
handler := HandleTodoCreate(&service) handler := HandleTodoCreate(logger, &service)
requestBody, err := json.Marshal(createTodoRequest) requestBody, err := json.Marshal(createTodoRequest)
@ -67,7 +70,7 @@ func TestCreateTodo(t *testing.T) {
}) })
t.Run("returns 400 with bad json", func(t *testing.T) { t.Run("returns 400 with bad json", func(t *testing.T) {
handler := HandleTodoCreate(nil) handler := HandleTodoCreate(logger, nil)
badStruct := struct { badStruct := struct {
Foo string Foo string
@ -95,12 +98,12 @@ func TestCreateTodo(t *testing.T) {
createTodoRequest := CreateTodoRequest{Name: "clean dishes", Done: false} createTodoRequest := CreateTodoRequest{Name: "clean dishes", Done: false}
service := MockTodoCreater{ service := MockTodoCreater{
CreateTodoFunc: func(todo service.Todo) (service.Todo, error) { CreateTodoFunc: func(ctx context.Context, todo service.Todo) (service.Todo, error) {
return service.Todo{}, errors.New("foo bar") return service.Todo{}, errors.New("foo bar")
}, },
} }
handler := HandleTodoCreate(&service) handler := HandleTodoCreate(logger, &service)
requestBody, err := json.Marshal(createTodoRequest) requestBody, err := json.Marshal(createTodoRequest)

View File

@ -1,7 +1,9 @@
package handler package handler
import ( import (
"context"
"errors" "errors"
"log/slog"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
@ -10,22 +12,23 @@ import (
) )
type MockTodoDeleter struct { type MockTodoDeleter struct {
DeleteTodoFunc func(id int64) error DeleteTodoFunc func(ctx context.Context, id int64) error
} }
func (tg *MockTodoDeleter) DeleteTodo(id int64) error { func (tg *MockTodoDeleter) DeleteTodo(ctx context.Context, id int64) error {
return tg.DeleteTodoFunc(id) return tg.DeleteTodoFunc(ctx, id)
} }
func TestDeleteTodo(t *testing.T) { func TestDeleteTodo(t *testing.T) {
logger := slog.Default()
t.Run("deletes existing todo", func(t *testing.T) { t.Run("deletes existing todo", func(t *testing.T) {
service := MockTodoDeleter{ service := MockTodoDeleter{
DeleteTodoFunc: func(id int64) error { DeleteTodoFunc: func(ctx context.Context, id int64) error {
return nil return nil
}, },
} }
handler := HandleTodoDelete(&service) handler := HandleTodoDelete(logger, &service)
req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil) req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()
@ -39,7 +42,7 @@ func TestDeleteTodo(t *testing.T) {
}) })
t.Run("returns 400 with bad id", func(t *testing.T) { t.Run("returns 400 with bad id", func(t *testing.T) {
handler := HandleTodoDelete(nil) handler := HandleTodoDelete(logger, nil)
req := httptest.NewRequest(http.MethodDelete, "/todo/hello", nil) req := httptest.NewRequest(http.MethodDelete, "/todo/hello", nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()
@ -54,12 +57,12 @@ func TestDeleteTodo(t *testing.T) {
t.Run("returns 404 for not found todo", func(t *testing.T) { t.Run("returns 404 for not found todo", func(t *testing.T) {
service := MockTodoDeleter{ service := MockTodoDeleter{
DeleteTodoFunc: func(id int64) error { DeleteTodoFunc: func(ctx context.Context, id int64) error {
return service.ErrNotFound return service.ErrNotFound
}, },
} }
handler := HandleTodoDelete(&service) handler := HandleTodoDelete(logger, &service)
req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil) req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()
@ -74,12 +77,12 @@ func TestDeleteTodo(t *testing.T) {
t.Run("returns 500 for arbitrary errors", func(t *testing.T) { t.Run("returns 500 for arbitrary errors", func(t *testing.T) {
service := MockTodoDeleter{ service := MockTodoDeleter{
DeleteTodoFunc: func(id int64) error { DeleteTodoFunc: func(ctx context.Context, id int64) error {
return errors.New("foo bar") return errors.New("foo bar")
}, },
} }
handler := HandleTodoDelete(&service) handler := HandleTodoDelete(logger, &service)
req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil) req := httptest.NewRequest(http.MethodDelete, "/todo/1", nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()

View File

@ -1,9 +1,11 @@
package handler package handler
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
@ -13,25 +15,26 @@ import (
) )
type MockTodoGetter struct { type MockTodoGetter struct {
GetTodoFunc func(id int64) (service.Todo, error) GetTodoFunc func(ctx context.Context, id int64) (service.Todo, error)
} }
func (tg *MockTodoGetter) GetTodo(id int64) (service.Todo, error) { func (tg *MockTodoGetter) GetTodo(ctx context.Context, id int64) (service.Todo, error) {
return tg.GetTodoFunc(id) return tg.GetTodoFunc(ctx, id)
} }
func TestGetTodo(t *testing.T) { func TestGetTodo(t *testing.T) {
logger := slog.Default()
t.Run("gets existing todo", func(t *testing.T) { t.Run("gets existing todo", func(t *testing.T) {
todo := service.Todo{Id: 1, Name: "clean dishes", Done: false} todo := service.Todo{Id: 1, Name: "clean dishes", Done: false}
wantedTodo := GetTodoResponse{Id: 1, Name: "clean dishes", Done: false} wantedTodo := GetTodoResponse{Id: 1, Name: "clean dishes", Done: false}
service := MockTodoGetter{ service := MockTodoGetter{
GetTodoFunc: func(id int64) (service.Todo, error) { GetTodoFunc: func(ctx context.Context, id int64) (service.Todo, error) {
return todo, nil return todo, nil
}, },
} }
handler := HandleTodoGet(&service) handler := HandleTodoGet(logger, &service)
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/todo/%d", todo.Id), nil) req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/todo/%d", todo.Id), nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()
@ -56,7 +59,7 @@ func TestGetTodo(t *testing.T) {
}) })
t.Run("returns 400 with bad id", func(t *testing.T) { t.Run("returns 400 with bad id", func(t *testing.T) {
handler := HandleTodoGet(nil) handler := HandleTodoGet(logger, nil)
req := httptest.NewRequest(http.MethodGet, "/todo/hello", nil) req := httptest.NewRequest(http.MethodGet, "/todo/hello", nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()
@ -71,12 +74,12 @@ func TestGetTodo(t *testing.T) {
t.Run("returns 404 for not found todo", func(t *testing.T) { t.Run("returns 404 for not found todo", func(t *testing.T) {
service := MockTodoGetter{ service := MockTodoGetter{
GetTodoFunc: func(id int64) (service.Todo, error) { GetTodoFunc: func(ctx context.Context, id int64) (service.Todo, error) {
return service.Todo{}, service.ErrNotFound return service.Todo{}, service.ErrNotFound
}, },
} }
handler := HandleTodoGet(&service) handler := HandleTodoGet(logger, &service)
req := httptest.NewRequest(http.MethodGet, "/todo/1", nil) req := httptest.NewRequest(http.MethodGet, "/todo/1", nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()
@ -91,12 +94,12 @@ func TestGetTodo(t *testing.T) {
t.Run("returns 500 for arbitrary errors", func(t *testing.T) { t.Run("returns 500 for arbitrary errors", func(t *testing.T) {
service := MockTodoGetter{ service := MockTodoGetter{
GetTodoFunc: func(id int64) (service.Todo, error) { GetTodoFunc: func(ctx context.Context, id int64) (service.Todo, error) {
return service.Todo{}, errors.New("foo bar") return service.Todo{}, errors.New("foo bar")
}, },
} }
handler := HandleTodoGet(&service) handler := HandleTodoGet(logger, &service)
req := httptest.NewRequest(http.MethodGet, "/todo/1", nil) req := httptest.NewRequest(http.MethodGet, "/todo/1", nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()

View File

@ -2,7 +2,9 @@ package handler
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"log/slog"
"errors" "errors"
"net/http" "net/http"
@ -13,24 +15,25 @@ import (
) )
type MockTodoUpdater struct { type MockTodoUpdater struct {
UpdateTodoFunc func(todo service.Todo) error UpdateTodoFunc func(ctx context.Context, todo service.Todo) error
} }
func (tg *MockTodoUpdater) UpdateTodo(todo service.Todo) error { func (tg *MockTodoUpdater) UpdateTodo(ctx context.Context, todo service.Todo) error {
return tg.UpdateTodoFunc(todo) return tg.UpdateTodoFunc(ctx, todo)
} }
func TestUpdateTodo(t *testing.T) { func TestUpdateTodo(t *testing.T) {
logger := slog.Default()
t.Run("update todo", func(t *testing.T) { t.Run("update todo", func(t *testing.T) {
updateTodoRequest := UpdateTodoRequest{Name: "clean dishes", Done: false} updateTodoRequest := UpdateTodoRequest{Name: "clean dishes", Done: false}
service := MockTodoUpdater{ service := MockTodoUpdater{
UpdateTodoFunc: func(todo service.Todo) error { UpdateTodoFunc: func(ctx context.Context, todo service.Todo) error {
return nil return nil
}, },
} }
handler := HandleTodoUpdate(&service) handler := HandleTodoUpdate(logger, &service)
requestBody, err := json.Marshal(updateTodoRequest) requestBody, err := json.Marshal(updateTodoRequest)
@ -50,7 +53,7 @@ func TestUpdateTodo(t *testing.T) {
}) })
t.Run("returns 400 with bad json", func(t *testing.T) { t.Run("returns 400 with bad json", func(t *testing.T) {
handler := HandleTodoUpdate(nil) handler := HandleTodoUpdate(logger, nil)
badStruct := struct { badStruct := struct {
Foo string Foo string
@ -75,7 +78,7 @@ func TestUpdateTodo(t *testing.T) {
}) })
t.Run("returns 400 with bad id", func(t *testing.T) { t.Run("returns 400 with bad id", func(t *testing.T) {
handler := HandleTodoUpdate(nil) handler := HandleTodoUpdate(logger, nil)
req := httptest.NewRequest(http.MethodPut, "/todo/hello", nil) req := httptest.NewRequest(http.MethodPut, "/todo/hello", nil)
res := httptest.NewRecorder() res := httptest.NewRecorder()
@ -92,12 +95,12 @@ func TestUpdateTodo(t *testing.T) {
updateTodoRequest := UpdateTodoRequest{Name: "clean dishes", Done: false} updateTodoRequest := UpdateTodoRequest{Name: "clean dishes", Done: false}
service := MockTodoUpdater{ service := MockTodoUpdater{
UpdateTodoFunc: func(todo service.Todo) error { UpdateTodoFunc: func(ctx context.Context, todo service.Todo) error {
return errors.New("foo bar") return errors.New("foo bar")
}, },
} }
handler := HandleTodoUpdate(&service) handler := HandleTodoUpdate(logger, &service)
requestBody, err := json.Marshal(updateTodoRequest) requestBody, err := json.Marshal(updateTodoRequest)

View File

@ -9,13 +9,13 @@ import (
type PostgresTodoRepository struct { type PostgresTodoRepository struct {
logger *slog.Logger logger *slog.Logger
db *sql.DB db *sql.DB
} }
func NewPostgresTodoRepository(logger *slog.Logger, db *sql.DB) *PostgresTodoRepository { func NewPostgresTodoRepository(logger *slog.Logger, db *sql.DB) *PostgresTodoRepository {
return &PostgresTodoRepository{ return &PostgresTodoRepository{
logger: logger, logger: logger,
db: db, db: db,
} }
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"log/slog"
"testing" "testing"
"time" "time"
@ -64,9 +65,10 @@ func (tdb *TestDatabase) TearDown() {
} }
func TestCRUD(t *testing.T) { func TestCRUD(t *testing.T) {
logger := slog.Default()
tdb := NewTestDatabase(t) tdb := NewTestDatabase(t)
defer tdb.TearDown() defer tdb.TearDown()
r := NewPostgresTodoRepository(tdb.Db) r := NewPostgresTodoRepository(logger, tdb.Db)
t.Run("creates new todo", func(t *testing.T) { t.Run("creates new todo", func(t *testing.T) {
want := repository.TodoRow{Id: 1, Name: "clean dishes", Done: false} want := repository.TodoRow{Id: 1, Name: "clean dishes", Done: false}

View File

@ -43,13 +43,13 @@ type TodoRepository interface {
type TodoService struct { type TodoService struct {
logger *slog.Logger logger *slog.Logger
repo TodoRepository repo TodoRepository
} }
func NewTodoService(logger *slog.Logger, todoRepo TodoRepository) *TodoService { func NewTodoService(logger *slog.Logger, todoRepo TodoRepository) *TodoService {
return &TodoService{ return &TodoService{
logger: logger, logger: logger,
repo: todoRepo, repo: todoRepo,
} }
} }
@ -88,7 +88,7 @@ func (s *TodoService) DeleteTodo(ctx context.Context, id int64) error {
return ErrNotFound return ErrNotFound
} }
if (err != nil) { if err != nil {
s.logger.ErrorContext(ctx, err.Error()) s.logger.ErrorContext(ctx, err.Error())
} }
@ -104,7 +104,7 @@ func (s *TodoService) UpdateTodo(ctx context.Context, todo Todo) error {
return ErrNotFound return ErrNotFound
} }
if (err != nil) { if err != nil {
s.logger.ErrorContext(ctx, err.Error()) s.logger.ErrorContext(ctx, err.Error())
} }

View File

@ -1,6 +1,8 @@
package service_test package service_test
import ( import (
"context"
"log/slog"
"testing" "testing"
"gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository" "gitea.michaelthomson.dev/mthomson/habits/internal/todo/repository"
@ -9,74 +11,83 @@ import (
) )
func TestCreateTodo(t *testing.T) { func TestCreateTodo(t *testing.T) {
ctx := context.Background()
logger := slog.Default()
todoRepository := inmemory.NewInMemoryTodoRepository() todoRepository := inmemory.NewInMemoryTodoRepository()
todoService := service.NewTodoService(&todoRepository) todoService := service.NewTodoService(logger, &todoRepository)
t.Run("Create todo", func(t *testing.T) { t.Run("Create todo", func(t *testing.T) {
todo := service.NewTodo("clean dishes", false) todo := service.NewTodo("clean dishes", false)
_, err := todoService.CreateTodo(todo) _, err := todoService.CreateTodo(ctx, todo)
AssertNoError(t, err) AssertNoError(t, err)
}) })
} }
func TestGetTodo(t *testing.T) { func TestGetTodo(t *testing.T) {
ctx := context.Background()
logger := slog.Default()
todoRepository := inmemory.NewInMemoryTodoRepository() todoRepository := inmemory.NewInMemoryTodoRepository()
todoRepository.Db[1] = repository.TodoRow{Id: 1, Name: "clean dishes", Done: false} todoRepository.Db[1] = repository.TodoRow{Id: 1, Name: "clean dishes", Done: false}
todoService := service.NewTodoService(&todoRepository) todoService := service.NewTodoService(logger, &todoRepository)
t.Run("Get exisiting todo", func(t *testing.T) { t.Run("Get exisiting todo", func(t *testing.T) {
_, err := todoService.GetTodo(1) _, err := todoService.GetTodo(ctx, 1)
AssertNoError(t, err) AssertNoError(t, err)
}) })
t.Run("Get non-existant todo", func(t *testing.T) { t.Run("Get non-existant todo", func(t *testing.T) {
_, err := todoService.GetTodo(2) _, err := todoService.GetTodo(ctx, 2)
AssertErrors(t, err, service.ErrNotFound) AssertErrors(t, err, service.ErrNotFound)
}) })
} }
func TestDeleteTodo(t *testing.T) { func TestDeleteTodo(t *testing.T) {
ctx := context.Background()
logger := slog.Default()
todoRepository := inmemory.NewInMemoryTodoRepository() todoRepository := inmemory.NewInMemoryTodoRepository()
todoRepository.Db[1] = repository.TodoRow{Id: 1, Name: "clean dishes", Done: false} todoRepository.Db[1] = repository.TodoRow{Id: 1, Name: "clean dishes", Done: false}
todoService := service.NewTodoService(&todoRepository) todoService := service.NewTodoService(logger, &todoRepository)
t.Run("Delete exisiting todo", func(t *testing.T) { t.Run("Delete exisiting todo", func(t *testing.T) {
err := todoService.DeleteTodo(1) err := todoService.DeleteTodo(ctx, 1)
AssertNoError(t, err) AssertNoError(t, err)
}) })
t.Run("Delete non-existant todo", func(t *testing.T) { t.Run("Delete non-existant todo", func(t *testing.T) {
err := todoService.DeleteTodo(1) err := todoService.DeleteTodo(ctx, 1)
AssertErrors(t, err, service.ErrNotFound) AssertErrors(t, err, service.ErrNotFound)
}) })
} }
func TestUpdateTodo(t *testing.T) { func TestUpdateTodo(t *testing.T) {
ctx := context.Background()
logger := slog.Default()
todoRepository := inmemory.NewInMemoryTodoRepository() todoRepository := inmemory.NewInMemoryTodoRepository()
todoRepository.Db[1] = repository.TodoRow{Id: 1, Name: "clean dishes", Done: false} todoRepository.Db[1] = repository.TodoRow{Id: 1, Name: "clean dishes", Done: false}
todoService := service.NewTodoService(&todoRepository) todoService := service.NewTodoService(logger, &todoRepository)
t.Run("Update exisiting todo", func(t *testing.T) { t.Run("Update exisiting todo", func(t *testing.T) {
todo := service.Todo{1, "clean dishes", true} todo := service.Todo{1, "clean dishes", true}
err := todoService.UpdateTodo(todo) err := todoService.UpdateTodo(ctx, todo)
AssertNoError(t, err) AssertNoError(t, err)
newTodo, err := todoService.GetTodo(1) newTodo, err := todoService.GetTodo(ctx, 1)
AssertNoError(t, err) AssertNoError(t, err)
@ -86,7 +97,7 @@ func TestUpdateTodo(t *testing.T) {
t.Run("Update non-existant todo", func(t *testing.T) { t.Run("Update non-existant todo", func(t *testing.T) {
todo := service.Todo{2, "clean dishes", true} todo := service.Todo{2, "clean dishes", true}
err := todoService.UpdateTodo(todo) err := todoService.UpdateTodo(ctx, todo)
AssertErrors(t, err, service.ErrNotFound) AssertErrors(t, err, service.ErrNotFound)
}) })