Echo Response Assertions
Introduction
When developing APIs with Echo, it's not enough to just send requests and get responses — you need to validate that those responses match your expectations. This is where response assertions come in. Response assertions are checks that verify if your API responses contain the expected status codes, headers, body content, or structure.
In this tutorial, we'll explore how to use assertions in Echo tests to validate API responses thoroughly. You'll learn how to write assertions that ensure your API is returning exactly what you expect it to return.
What are Echo Response Assertions?
Response assertions are statements that validate whether a response from your Echo server matches predetermined criteria. These assertions check various aspects of the response:
- Status codes (200 OK, 404 Not Found, etc.)
- Response headers
- Response body content
- JSON structure and values
- Response timing
Assertions allow us to automate the verification process, making our tests reliable and repeatable.
Basic Assertion Types
Let's start with the most common types of assertions you'll use in Echo testing.
Status Code Assertions
One of the most fundamental checks is verifying that your API returns the expected HTTP status code.
func TestHomeEndpoint(t *testing.T) {
// Setup Echo
e := echo.New()
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
// Make request
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// Assert status code
assert.Equal(t, http.StatusOK, rec.Code)
}
In this example, we're asserting that the response status code is 200 (OK). If the handler returns any other status code, the test will fail.
Body Content Assertions
To verify the response body content, you can check the exact string or a pattern within the response:
func TestGreetingEndpoint(t *testing.T) {
e := echo.New()
e.GET("/greet/:name", func(c echo.Context) error {
name := c.Param("name")
return c.String(http.StatusOK, "Hello, " + name + "!")
})
// Make request
req := httptest.NewRequest(http.MethodGet, "/greet/John", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// Assert response body
assert.Equal(t, "Hello, John!", rec.Body.String())
// Or check if it contains the expected substring
assert.Contains(t, rec.Body.String(), "John")
}
Header Assertions
You can also check if specific response headers are set correctly:
func TestHeaderEndpoint(t *testing.T) {
e := echo.New()
e.GET("/json", func(c echo.Context) error {
c.Response().Header().Set("Content-Type", "application/json")
return c.JSON(http.StatusOK, map[string]string{"message": "success"})
})
// Make request
req := httptest.NewRequest(http.MethodGet, "/json", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// Assert header
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
}
JSON Response Assertions
When working with JSON responses, you'll often need to check specific fields or structures. Here's how you can validate JSON content:
func TestJSONResponse(t *testing.T) {
e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"id": c.Param("id"),
"name": "John Doe",
"email": "[email protected]",
"roles": []string{"user", "admin"},
})
})
// Make request
req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// Parse response
var response map[string]interface{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
// Assert JSON field values
assert.Equal(t, "123", response["id"])
assert.Equal(t, "John Doe", response["name"])
assert.Equal(t, "[email protected]", response["email"])
// Check array contents
roles, ok := response["roles"].([]interface{})
assert.True(t, ok)
assert.Contains(t, roles, "admin")
assert.Len(t, roles, 2)
}
Advanced Assertions
As your API becomes more complex, you might need more sophisticated assertions.
Pattern Matching with Regular Expressions
Sometimes you need to check if a response matches a pattern rather than an exact string:
func TestRegexPattern(t *testing.T) {
e := echo.New()
e.GET("/uuid", func(c echo.Context) error {
uuid := "550e8400-e29b-41d4-a716-446655440000" // Example UUID
return c.String(http.StatusOK, uuid)
})
// Make request
req := httptest.NewRequest(http.MethodGet, "/uuid", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// Assert using regex pattern for UUID
match, _ := regexp.MatchString(
`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`,
rec.Body.String(),
)
assert.True(t, match)
}
Conditional Assertions
Sometimes you need to perform different assertions based on certain conditions:
func TestConditionalResponse(t *testing.T) {
e := echo.New()
e.GET("/feature/:status", func(c echo.Context) error {
status := c.Param("status")
if status == "enabled" {
return c.JSON(http.StatusOK, map[string]interface{}{
"feature": "enabled",
"options": []string{"option1", "option2"},
})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"feature": "disabled",
})
})
// Test the enabled case
req := httptest.NewRequest(http.MethodGet, "/feature/enabled", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
var response map[string]interface{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
if response["feature"] == "enabled" {
options, exists := response["options"].([]interface{})
assert.True(t, exists)
assert.NotEmpty(t, options)
} else {
// Ensure no options field exists when disabled
_, exists := response["options"]
assert.False(t, exists)
}
}
Real-world Example: Testing a CRUD API
Let's put it all together with a more comprehensive example of testing a CRUD (Create, Read, Update, Delete) API:
func TestUserCRUD(t *testing.T) {
// Setup in-memory user storage
users := make(map[string]map[string]interface{})
// Setup Echo server
e := echo.New()
// Create user endpoint
e.POST("/users", func(c echo.Context) error {
user := make(map[string]interface{})
if err := c.Bind(&user); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request body",
})
}
id := fmt.Sprintf("%d", time.Now().UnixNano())
user["id"] = id
users[id] = user
return c.JSON(http.StatusCreated, user)
})
// Get user endpoint
e.GET("/users/:id", func(c echo.Context) error {
id := c.Param("id")
user, exists := users[id]
if !exists {
return c.JSON(http.StatusNotFound, map[string]string{
"error": "User not found",
})
}
return c.JSON(http.StatusOK, user)
})
// Test Create User
t.Run("Create User", func(t *testing.T) {
userData := `{"name":"Alice Smith","email":"[email protected]"}`
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(userData))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// Assert status code
assert.Equal(t, http.StatusCreated, rec.Code)
// Parse response
var response map[string]interface{}
err := json.Unmarshal(rec.Body.Bytes(), &response)
assert.NoError(t, err)
// Store user ID for subsequent tests
userID := response["id"].(string)
// Assert user data
assert.Equal(t, "Alice Smith", response["name"])
assert.Equal(t, "[email protected]", response["email"])
assert.NotEmpty(t, userID)
// Test Get User (using ID from creation)
t.Run("Get User", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/users/"+userID, nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// Assert status code
assert.Equal(t, http.StatusOK, rec.Code)
// Parse response
var getResponse map[string]interface{}
err := json.Unmarshal(rec.Body.Bytes(), &getResponse)
assert.NoError(t, err)
// Verify correct user returned
assert.Equal(t, userID, getResponse["id"])
assert.Equal(t, "Alice Smith", getResponse["name"])
assert.Equal(t, "[email protected]", getResponse["email"])
})
// Test Get Non-existent User
t.Run("Get Non-existent User", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/users/nonexistent", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
// Assert not found status
assert.Equal(t, http.StatusNotFound, rec.Code)
// Parse error response
var errorResponse map[string]interface{}
err := json.Unmarshal(rec.Body.Bytes(), &errorResponse)
assert.NoError(t, err)
// Verify error message
assert.Equal(t, "User not found", errorResponse["error"])
})
})
}
Best Practices for Response Assertions
- Test one thing per test: Keep each test focused on one specific functionality.
- Use descriptive test names: Make it clear what each test is checking.
- Test edge cases: Don't just test the "happy path"; also test error conditions.
- Test both structure and values: For JSON responses, check both the structure (fields exist) and the values.
- Use table-driven tests for similar endpoints with different inputs/outputs.
- Keep tests independent: One test should not depend on the outcome of another.
- Don't overly assert: Focus on what matters; asserting too many things makes tests brittle.
Common Assertion Mistakes
- Testing implementation details: Focus on testing behavior, not implementation.
- Hardcoding dynamic values: Be careful with timestamps, UUIDs, or other dynamic data.
- Ignoring error conditions: Make sure your tests cover error responses.
- Tightly coupling tests to response format: This makes refactoring difficult.
- Not testing for negative cases: Ensure your API handles invalid inputs correctly.
Summary
Echo response assertions are a critical part of building reliable APIs. By thoroughly validating responses, you ensure that your API behaves as expected and continues to do so even as you make changes.
In this guide, we've covered:
- Basic assertion types for status codes, body content, and headers
- JSON response validation techniques
- Advanced assertions with pattern matching and conditional logic
- A real-world example of testing a CRUD API
- Best practices and common mistakes to avoid
With these techniques, you can build comprehensive test suites that give you confidence that your Echo API works correctly.
Additional Resources and Exercises
Exercises
-
Basic Assertion Exercise: Create a basic Echo API with endpoints returning different status codes, and write tests that assert the correct status code for each.
-
JSON Validation Exercise: Build an endpoint that returns complex nested JSON, and write assertions to validate both the structure and specific values.
-
Error Handling Exercise: Create endpoints that can return different error responses and write tests that validate the appropriate error messages and codes.
-
Conditional Response Exercise: Build an endpoint whose behavior changes based on query parameters, and write tests that assert the correct response for each variation.
Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)