Skip to content

Commit

Permalink
testing documentation | testing api fixes and clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
garrettladley committed Jan 21, 2024
1 parent 62e6293 commit cd5d49d
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 49 deletions.
3 changes: 3 additions & 0 deletions backend/src/controllers/category.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package controllers

import (
"fmt"

"github.com/GenerateNU/sac/backend/src/models"
"github.com/GenerateNU/sac/backend/src/services"

Expand Down Expand Up @@ -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")
}

Expand Down
140 changes: 140 additions & 0 deletions backend/tests/api/README.md
Original file line number Diff line number Diff line change
@@ -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,
},
)
```
47 changes: 18 additions & 29 deletions backend/tests/api/category_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
},
)
}
Expand All @@ -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,
},
)
}
Expand Down
32 changes: 15 additions & 17 deletions backend/tests/api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand All @@ -171,30 +179,21 @@ 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)

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()
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 0 additions & 3 deletions backend/tests/api/tag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit cd5d49d

Please sign in to comment.