Lesson 3
Testing CRUD Operations with Setup and Teardown in Go
Testing CRUD Operations with Setup and Teardown

Welcome to another step in our journey to mastering automated API testing with Go. So far, you've learned how to organize tests using Go's testing package. Today, we will focus on automating tests for CRUD operations — Create, Read, Update, and Delete — which are integral actions for managing data in any application that uses RESTful APIs. Automated testing of these operations is essential to ensure that APIs function correctly and modify resources as expected. Thorough testing of CRUD operations will help you catch issues early and ensure API reliability.

Setup and Teardown in Go Testing

In automated testing, setup and teardown are fundamental concepts that help ensure each test has a clean start and finish. Setup involves preparing what you need before a test runs, like creating test data or setting up configurations. Teardown involves cleaning up afterward, removing any leftovers from the test, such as deleting test data, so future tests aren't affected. This process ensures tests don't interfere with each other.

In Go, setup and teardown can be handled manually within test functions. This involves writing setup code at the beginning of a test and cleanup code at the end. This ensures that each test starts with the necessary conditions and ends without leaving any trace.

Implementing Setup and Teardown in Go

To effectively manage setup and teardown for CRUD operations, we manually handle these processes within each test function. This ensures each test starts with the right conditions and ends without leaving any trace. By structuring our tests this way, we maintain a clean state, which is crucial to avoid any interference between tests.

Here's an example of how setup and teardown can be structured in Go:

Go
1package main 2 3import ( 4 "bytes" 5 "encoding/json" 6 "net/http" 7 "testing" 8 "strconv" 9) 10 11const baseURL = "http://localhost:8000" 12 13type Todo struct { 14 ID int `json:"id"` 15 Title string `json:"title"` 16 Description string `json:"description"` 17 Done bool `json:"done"` 18} 19 20func setup(t *testing.T) *Todo { 21 todoData := &Todo{ 22 Title: "Setup Todo", 23 Description: "For testing CRUD operations", 24 Done: false, 25 } 26 27 body, _ := json.Marshal(todoData) 28 req, _ := http.NewRequest("POST", baseURL+"/todos", bytes.NewBuffer(body)) 29 req.Header.Set("Content-Type", "application/json") 30 31 resp, err := http.DefaultClient.Do(req) 32 if err != nil || resp.StatusCode != http.StatusCreated { 33 t.Fatalf("Failed to create a todo for setup: %v", err) 34 } 35 36 defer resp.Body.Close() 37 json.NewDecoder(resp.Body).Decode(todoData) 38 return todoData 39} 40 41func teardown(t *testing.T, todoID string) { 42 req, _ := http.NewRequest("DELETE", baseURL+"/todos/"+todoID, nil) 43 resp, err := http.DefaultClient.Do(req) 44 if err != nil || resp.StatusCode != http.StatusNoContent { 45 t.Fatalf("Failed to delete the todo during teardown: %v", err) 46 } 47}

Within this setup function, we create a todo item before each test, ensuring the test environment has the necessary data. The teardown function deletes the created todo, maintaining a clean slate for subsequent tests. This ensures consistent, independent test runs free from interference caused by leftover data.

Defining CRUD Tests

With setup and teardown processes in place, we establish a consistent environment for our tests. Now, we'll define the specific tests for each CRUD operation. These tests will leverage the setup todo item, ensuring that we assess the ability to create, read, update, and delete resources accurately. Let's explore each operation through dedicated test functions.

Testing Read Operation

In our CRUD operations, the Read test checks if we can successfully retrieve a todo item. Using the http.Get method, we fetch the todo item created during the setup. The test verifies the HTTP status code is 200, indicating success, and it asserts that the data returned matches our expectations. This confirms that our API's read functionality works correctly.

Go
1func TestReadTodo(t *testing.T) { 2 // Setup 3 todo := setup(t) 4 defer teardown(t, strconv.Itoa(todo.ID)) 5 6 // Act 7 resp, err := http.Get(baseURL + "/todos/" + strconv.Itoa(todo.ID)) 8 if err != nil { 9 t.Fatalf("Failed to read todo: %v", err) 10 } 11 defer resp.Body.Close() 12 13 // Assert 14 if resp.StatusCode != http.StatusOK { 15 t.Fatalf("Expected status code 200, got %d", resp.StatusCode) 16 } 17 18 var fetchedTodo Todo 19 json.NewDecoder(resp.Body).Decode(&fetchedTodo) 20 if fetchedTodo.Title != todo.Title || fetchedTodo.Description != todo.Description || fetchedTodo.Done != todo.Done { 21 t.Fatalf("Fetched todo does not match expected values") 22 } 23}
Testing Update Operation with PATCH

The Update operation using PATCH focuses on modifying specific fields of a todo item. Here, we send a PATCH request to change the done status to True. The test checks if the status code is 200, confirming the update was successful. We also verify the field was accurately updated in the API. This ensures that partial updates with PATCH are functioning as intended.

Go
1func TestUpdateTodoWithPatch(t *testing.T) { 2 // Setup 3 todo := setup(t) 4 defer teardown(t, strconv.Itoa(todo.ID)) 5 6 // Arrange 7 updateData := map[string]bool{"done": true} 8 body, _ := json.Marshal(updateData) 9 10 // Act 11 req, _ := http.NewRequest("PATCH", baseURL+"/todos/"+strconv.Itoa(todo.ID), bytes.NewBuffer(body)) 12 req.Header.Set("Content-Type", "application/json") 13 resp, err := http.DefaultClient.Do(req) 14 if err != nil { 15 t.Fatalf("Failed to update todo: %v", err) 16 } 17 defer resp.Body.Close() 18 19 // Assert 20 if resp.StatusCode != http.StatusOK { 21 t.Fatalf("Expected status code 200, got %d", resp.StatusCode) 22 } 23 24 var updatedTodo Todo 25 json.NewDecoder(resp.Body).Decode(&updatedTodo) 26 if !updatedTodo.Done { 27 t.Fatalf("Expected todo done status to be true, got false") 28 } 29}
Testing Update Operation with PUT

For a complete replacement of fields, the Update operation using PUT is employed. This test sends a PUT request to replace all fields of the todo item with new data. We assert the status code is 200, indicating the operation succeeded, and confirm that all fields match the updated values. This test validates that full updates with PUT are correctly processed by the API.

Go
1func TestUpdateTodoWithPut(t *testing.T) { 2 // Setup 3 todo := setup(t) 4 defer teardown(t, strconv.Itoa(todo.ID)) 5 6 // Arrange 7 putData := &Todo{ 8 Title: "Updated Title", 9 Description: "Updated Description", 10 Done: true, 11 } 12 body, _ := json.Marshal(putData) 13 14 // Act 15 req, _ := http.NewRequest("PUT", baseURL+"/todos/"+strconv.Itoa(todo.ID), bytes.NewBuffer(body)) 16 req.Header.Set("Content-Type", "application/json") 17 resp, err := http.DefaultClient.Do(req) 18 if err != nil { 19 t.Fatalf("Failed to update todo: %v", err) 20 } 21 defer resp.Body.Close() 22 23 // Assert 24 if resp.StatusCode != http.StatusOK { 25 t.Fatalf("Expected status code 200, got %d", resp.StatusCode) 26 } 27 28 var updatedTodo Todo 29 json.NewDecoder(resp.Body).Decode(&updatedTodo) 30 if updatedTodo.Title != putData.Title || updatedTodo.Description != putData.Description || updatedTodo.Done != putData.Done { 31 t.Fatalf("Updated todo does not match expected values") 32 } 33}
Testing Delete Operation

The Delete test checks if a todo item can be removed successfully. A DELETE request is sent to the API, and the test verifies the status code is 204, signifying a successful deletion with no content returned. To confirm the deletion, we attempt to retrieve the same todo item and expect a 404 status code, indicating it no longer exists. This ensures the API's delete functionality behaves as expected.

Go
1func TestDeleteTodo(t *testing.T) { 2 // Setup 3 todo := setup(t) 4 5 // Act 6 req, _ := http.NewRequest("DELETE", baseURL+"/todos/"+strconv.Itoa(todo.ID), nil) 7 resp, err := http.DefaultClient.Do(req) 8 if err != nil { 9 t.Fatalf("Failed to delete todo: %v", err) 10 } 11 defer resp.Body.Close() 12 13 // Assert 14 if resp.StatusCode != http.StatusNoContent { 15 t.Fatalf("Expected status code 204, got %d", resp.StatusCode) 16 } 17 18 // Verify Deletion 19 resp, err = http.Get(baseURL + "/todos/" + strconv.Itoa(todo.ID)) 20 if err != nil { 21 t.Fatalf("Failed to verify deletion: %v", err) 22 } 23 defer resp.Body.Close() 24 25 if resp.StatusCode != http.StatusNotFound { 26 t.Fatalf("Expected status code 404, got %d", resp.StatusCode) 27 } 28}
Summary and Practice Preparation

In today's lesson, we've delved into the essentials of testing CRUD operations with setup and teardown using Go's testing practices. You've seen how automating these processes helps maintain a structured and reliable testing environment. Now, it's your turn to practice these concepts with hands-on exercises that will reinforce your understanding and confidence in applying these techniques. As you continue to build your skills, you'll be better equipped to ensure API robustness and reliability. Keep up the great work, and prepare to explore testing authenticated endpoints moving forward!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.