From cd5d49d902c012dfda3b901ef1374645a0f27887 Mon Sep 17 00:00:00 2001 From: garrettladley Date: Sun, 21 Jan 2024 13:29:16 -0500 Subject: [PATCH] testing documentation | testing api fixes and clean up --- backend/src/controllers/category.go | 3 + backend/tests/api/README.md | 140 ++++++++++++++++++++++++++++ backend/tests/api/category_test.go | 47 ++++------ backend/tests/api/helpers.go | 32 +++---- backend/tests/api/tag_test.go | 3 - 5 files changed, 176 insertions(+), 49 deletions(-) create mode 100644 backend/tests/api/README.md diff --git a/backend/src/controllers/category.go b/backend/src/controllers/category.go index 321b83621..02ad115bf 100644 --- a/backend/src/controllers/category.go +++ b/backend/src/controllers/category.go @@ -1,6 +1,8 @@ package controllers import ( + "fmt" + "github.com/GenerateNU/sac/backend/src/models" "github.com/GenerateNU/sac/backend/src/services" @@ -32,6 +34,7 @@ func (t *CategoryController) CreateCategory(c *fiber.Ctx) error { var categoryBody models.CreateCategoryRequestBody if err := c.BodyParser(&categoryBody); err != nil { + fmt.Print(err) return fiber.NewError(fiber.StatusBadRequest, "failed to process the request") } diff --git a/backend/tests/api/README.md b/backend/tests/api/README.md new file mode 100644 index 000000000..6621648c3 --- /dev/null +++ b/backend/tests/api/README.md @@ -0,0 +1,140 @@ +# Using the Integration Testing Helpers + +The integration testing helpers are a set of functions that reduce the boilerplate code required to write integration tests. They are located in the `backend/tests/helpers.go`. + +## Modeling a Request with `TestRequest` + +You can model a request with the `TestRequest` struct: + +```go +type TestRequest struct { + Method string + Path string + Body *map[string]interface{} + Headers *map[string]string +} +``` + +Since `Body` and `Headers` are pointers, if they don't set them when creating a `TestRequest`, they will be `nil`. + +Here is an example of creating a `TestRequest`, notice how instead of saying `Headers: nil`, we can simply omit the `Headers` field. + +```go +TestRequest{ + Method: "POST", + Path: "/api/v1/tags/", + Body: &map[string]interface{}{ + "name": tagName, + "category_id": 1, + }, +} +``` + +This handles a lot of the logic for you, for example, if the body is not nil, it will be marshalled into JSON and the `Content-Type` header will be set to `application/json`. + +## Testing that a Request Returns a XXX Status Code + +Say you want to test hitting the `[APP_ADDRESS]/health` endpoint with a GET request returns a `200` status code. + +```go +TestRequest{ + Method: "GET", + Path: "/health", + }.TestOnStatus(t, nil, 200) +``` + +## Testing that a Request Returns a XXX Status Code and Assert Something About the Database + +Say you want to test that a creating a catgory with POST `[APP_ADDRESS]/api/v1/categories/` returns a `201` + +```go +TestRequest{ + Method: "POST", + Path: "/api/v1/categories/", + Body: &map[string]interface{}{ + "category_name": categoryName, + }, + }.TestOnStatusAndDB(t, nil, + DBTesterWithStatus{ + Status: 201, + DBTester: AssertRespCategorySameAsDBCategory, + }, + ) +``` + +### DBTesters + +Often times there are common assertions you want to make about the database, for example, if the object in the response is the same as the object in the database. We can create a lambda function that takes in the `TestApp`, `*assert.A`, and `*http.Response` and makes the assertions we want. We can then pass this function to the `DBTesterWithStatus` struct. + +```go +var AssertRespCategorySameAsDBCategory = func(app TestApp, assert *assert.A, resp *http.Response) { + var respCategory models.Category + + err := json.NewDecoder(resp.Body).Decode(&respCategory) + + assert.NilError(err) + + dbCategory, err := transactions.GetCategory(app.Conn, respCategory.ID) + + assert.NilError(err) + + assert.Equal(dbCategory, &respCategory) +} +``` + +### Existing App Asserts + +Since the test suite creates a new database for each test, we can have a deterministic database state for each test. However, what if we have a multi step test that depends on the previous steps database state? That is where `ExistingAppAssert` comes in! This will allow us to keep using the database from a previous step in the test. + +Consider this example, to create a tag, we need to create a category first. This is a multi step test, so we need to use `ExistingAppAssert` to keep the database state from the previous step. + +```go +appAssert := TestRequest{ + Method: "POST", + Path: "/api/v1/categories/", + Body: &map[string]interface{}{ + "category_name": categoryName, + }, + }.TestOnStatusAndDB(t, nil, + DBTesterWithStatus{ + Status: 201, + DBTester: AssertRespCategorySameAsDBCategory, + }, + ) + +TestRequest{ + Method: "POST", + Path: "/api/v1/tags/", + Body: &map[string]interface{}{ + "name": tagName, + "category_id": 1, + }, + }.TestOnStatusAndDB(t, &appAssert, + DBTesterWithStatus{ + Status: 201, + DBTester: AssertRespTagSameAsDBTag, + }, + ) +``` + +## Testing that a Request Returns a XXX Status Code, Assert Something About the Message, and Assert Something About the Database + +Say you want to test a bad request to POST `[APP_ADDRESS]/api/v1/categories/` endpoint returns a `400` status code, the message is `failed to process the request`, and that a category was not created. + +```go +TestRequest{ + Method: "POST", + Path: "/api/v1/categories/", + Body: &map[string]interface{}{ + "category_name": 1231, + }, + }.TestOnStatusMessageAndDB(t, nil, + StatusMessageDBTester{ + MessageWithStatus: MessageWithStatus{ + Status: 400, + Message: "failed to process the request", + }, + DBTester: AssertNoCategories, + }, + ) +``` diff --git a/backend/tests/api/category_test.go b/backend/tests/api/category_test.go index 83e6e0b2d..6a11fa7a3 100644 --- a/backend/tests/api/category_test.go +++ b/backend/tests/api/category_test.go @@ -11,6 +11,20 @@ import ( "github.com/goccy/go-json" ) +var AssertRespCategorySameAsDBCategory = func(app TestApp, assert *assert.A, resp *http.Response) { + var respCategory models.Category + + err := json.NewDecoder(resp.Body).Decode(&respCategory) + + assert.NilError(err) + + dbCategory, err := transactions.GetCategory(app.Conn, respCategory.ID) + + assert.NilError(err) + + assert.Equal(dbCategory, &respCategory) +} + func CreateSampleCategory(t *testing.T, categoryName string, existingAppAssert *ExistingAppAssert) ExistingAppAssert { return TestRequest{ Method: "POST", @@ -20,21 +34,8 @@ func CreateSampleCategory(t *testing.T, categoryName string, existingAppAssert * }, }.TestOnStatusAndDB(t, existingAppAssert, DBTesterWithStatus{ - Status: 201, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - - var respCategory models.Category - - err := json.NewDecoder(resp.Body).Decode(&respCategory) - - assert.NilError(err) - - dbCategory, err := transactions.GetCategory(app.Conn, respCategory.ID) - - assert.NilError(err) - - assert.Equal(dbCategory, &respCategory) - }, + Status: 201, + DBTester: AssertRespCategorySameAsDBCategory, }, ) } @@ -53,20 +54,8 @@ func TestCreateCategoryIgnoresid(t *testing.T) { }, }.TestOnStatusAndDB(t, nil, DBTesterWithStatus{ - Status: 201, - DBTester: func(app TestApp, assert *assert.A, resp *http.Response) { - var respCategory models.Category - - err := json.NewDecoder(resp.Body).Decode(&respCategory) - - assert.NilError(err) - - dbCategory, err := transactions.GetCategory(app.Conn, respCategory.ID) - - assert.NilError(err) - - assert.NotEqual(12, dbCategory.ID) - }, + Status: 201, + DBTester: AssertRespCategorySameAsDBCategory, }, ) } diff --git a/backend/tests/api/helpers.go b/backend/tests/api/helpers.go index 0d552d933..7cd5f131c 100644 --- a/backend/tests/api/helpers.go +++ b/backend/tests/api/helpers.go @@ -152,10 +152,18 @@ func (request TestRequest) Test(t *testing.T, existingAppAssert *ExistingAppAsse req = httptest.NewRequest(request.Method, address, bytes.NewBuffer(bodyBytes)) - if request.Headers != nil { - for key, value := range *request.Headers { - req.Header.Set(key, value) - } + if request.Headers == nil { + request.Headers = &map[string]string{} + } + + if _, ok := (*request.Headers)["Content-Type"]; !ok { + (*request.Headers)["Content-Type"] = "application/json" + } + } + + if request.Headers != nil { + for key, value := range *request.Headers { + req.Header.Add(key, value) } } @@ -171,6 +179,7 @@ func (request TestRequest) Test(t *testing.T, existingAppAssert *ExistingAppAsse func (request TestRequest) TestOnStatus(t *testing.T, existingAppAssert *ExistingAppAssert, status int) ExistingAppAssert { appAssert, resp := request.Test(t, existingAppAssert) + _, assert := appAssert.App, appAssert.Assert assert.Equal(status, resp.StatusCode) @@ -178,23 +187,13 @@ func (request TestRequest) TestOnStatus(t *testing.T, existingAppAssert *Existin return appAssert } -func (request TestRequest) TestWithJSONBody(t *testing.T, existingAppAssert *ExistingAppAssert) (ExistingAppAssert, *http.Response) { - if request.Headers == nil { - request.Headers = &map[string]string{"Content-Type": "application/json"} - } else if _, ok := (*request.Headers)["Content-Type"]; !ok { - (*request.Headers)["Content-Type"] = "application/json" - } - - return request.Test(t, existingAppAssert) -} - type MessageWithStatus struct { Status int Message string } func (request TestRequest) TestOnStatusAndMessage(t *testing.T, existingAppAssert *ExistingAppAssert, messagedStatus MessageWithStatus) ExistingAppAssert { - appAssert, resp := request.TestWithJSONBody(t, existingAppAssert) + appAssert, resp := request.Test(t, existingAppAssert) assert := appAssert.Assert defer resp.Body.Close() @@ -231,9 +230,8 @@ type DBTesterWithStatus struct { } func (request TestRequest) TestOnStatusAndDB(t *testing.T, existingAppAssert *ExistingAppAssert, dbTesterStatus DBTesterWithStatus) ExistingAppAssert { - appAssert, resp := request.TestWithJSONBody(t, existingAppAssert) + appAssert, resp := request.Test(t, existingAppAssert) app, assert := appAssert.App, appAssert.Assert - defer resp.Body.Close() assert.Equal(dbTesterStatus.Status, resp.StatusCode) diff --git a/backend/tests/api/tag_test.go b/backend/tests/api/tag_test.go index 2b35f7820..169152fb4 100644 --- a/backend/tests/api/tag_test.go +++ b/backend/tests/api/tag_test.go @@ -19,9 +19,6 @@ var AssertRespTagSameAsDBTag = func(app TestApp, assert *assert.A, resp *http.Re assert.NilError(err) - fmt.Printf("respTag: %+v\n", respTag) - fmt.Printf("respTag.ID: %+v\n", respTag.ID) - dbTag, err := transactions.GetTag(app.Conn, respTag.ID) assert.NilError(err)