Contrib Packages
Stable v1.4.8
Testing
Learn about Testing in EchoNext.
Testing
EchoNext provides optional testing utilities through the pkg/contrib/testing package. These utilities make it easy to write comprehensive tests for your EchoNext applications.
Overview
The testing package provides:
- APIClient - Fluent HTTP client for testing API endpoints
- Response - Rich response object with assertions
- Suite - Base test suite with common setup/teardown
- IntegrationSuite - Suite with transaction rollback support
- FixtureManager - Manage test data in the database
- Factory - Generic factory pattern for creating test entities
Installation
go get github.com/abdussamadbello/echonext/pkg/contrib/testing@v1.4.8APIClient
The APIClient provides a convenient, fluent interface for testing HTTP endpoints.
Basic Usage
import (
"testing"
"github.com/abdussamadbello/echonext"
echonexttest "github.com/abdussamadbello/echonext/pkg/contrib/testing"
)
func TestAPI(t *testing.T) {
app := echonext.New()
// ... register routes ...
client := echonexttest.NewAPIClient(app)
// Make a GET request
resp := client.GET("/users")
// Check response
if resp.Status() != 200 {
t.Errorf("Expected 200, got %d", resp.Status())
}
}HTTP Methods
client := echonexttest.NewAPIClient(app)
// GET request
resp := client.GET("/users")
// POST request with body
resp := client.POST("/users", map[string]string{
"name": "John",
"email": "john@example.com",
})
// PUT request with body
resp := client.PUT("/users/123", map[string]string{
"name": "John Updated",
})
// PATCH request with body
resp := client.PATCH("/users/123", map[string]string{
"email": "new@example.com",
})
// DELETE request
resp := client.DELETE("/users/123")Authentication
// Bearer token authentication
client := echonexttest.NewAPIClient(app).WithAuth("my-jwt-token")
resp := client.GET("/protected")
// Basic authentication
client := echonexttest.NewAPIClient(app).WithBasicAuth("user", "pass")
resp := client.GET("/protected")Custom Headers
client := echonexttest.NewAPIClient(app).
WithHeader("X-API-Key", "secret").
WithHeader("X-Custom", "value")
resp := client.GET("/api/data")Chaining
client := echonexttest.NewAPIClient(app).
WithAuth("token123").
WithHeader("X-Request-ID", "test-123").
WithHeader("Accept-Language", "en-US")
resp := client.POST("/orders", orderRequest)Response
The Response object provides methods for inspecting and asserting on HTTP responses.
Basic Methods
resp := client.GET("/users")
// Get status code
status := resp.Status()
// Get response body as string
body := resp.String()
// Check for errors
if err := resp.Error(); err != nil {
t.Fatal(err)
}JSON Parsing
resp := client.GET("/users")
// Parse into struct
var result echonext.Response[[]User]
err := resp.JSON(&result)
if err != nil {
t.Fatal(err)
}
fmt.Println(result.Data[0].Name)Status Checks
resp := client.GET("/users")
// Check if successful (2xx)
if resp.IsSuccess() {
// Handle success
}
// Check if error (4xx or 5xx)
if resp.IsError() {
// Handle error
}Headers
resp := client.GET("/users")
// Get specific header
contentType := resp.GetHeader("Content-Type")
requestID := resp.GetHeader("X-Request-ID")Assertions
Use built-in assertions for cleaner tests:
resp := client.GET("/users")
// Assert status code
resp.AssertStatus(t, 200)
// Assert successful response (2xx)
resp.AssertSuccess(t)
// Assert error response (4xx or 5xx)
resp.AssertError(t)
// Assert JSON content matches
expected := echonext.Response[[]User]{
Success: true,
Data: []User{
{ID: "1", Name: "John", Email: "john@example.com"},
},
}
resp.AssertJSON(t, expected)Chained Assertions
client.GET("/users").
AssertStatus(t, 200).
AssertSuccess(t)
client.POST("/users", invalidData).
AssertStatus(t, 400).
AssertError(t)Test Suites
Basic Suite
The Suite provides a base for organizing tests with common setup/teardown:
import (
"testing"
"github.com/abdussamadbello/echonext"
echonexttest "github.com/abdussamadbello/echonext/pkg/contrib/testing"
)
type UserSuite struct {
*echonexttest.Suite
}
func NewUserSuite(app *echonext.App, db *gorm.DB) *UserSuite {
return &UserSuite{
Suite: echonexttest.NewSuite(app, db),
}
}
func (s *UserSuite) Setup() error {
// Load test fixtures
return s.LoadFixtures(
&User{ID: 1, Name: "Test User", Email: "test@example.com"},
)
}
func (s *UserSuite) Teardown() error {
return s.Fixtures.Clear()
}
func TestUserAPI(t *testing.T) {
app, db := setupTestApp()
suite := NewUserSuite(app, db)
suite.Setup()
defer suite.Teardown()
// Use the client
resp := suite.Client.GET("/users")
resp.AssertSuccess(t)
}IntegrationSuite
The IntegrationSuite wraps each test in a transaction that rolls back:
type UserIntegrationSuite struct {
*echonexttest.IntegrationSuite
}
func NewUserIntegrationSuite(app *echonext.App, db *gorm.DB) *UserIntegrationSuite {
return &UserIntegrationSuite{
IntegrationSuite: echonexttest.NewIntegrationSuite(app, db),
}
}
func TestUserIntegration(t *testing.T) {
app, db := setupTestApp()
suite := NewUserIntegrationSuite(app, db)
// Start transaction
suite.BeginTx()
defer suite.RollbackTx() // Always rolls back
// Load fixtures (within transaction)
suite.LoadFixtures(&User{Name: "Test"})
// Run test
resp := suite.Client.POST("/users", createUserRequest)
resp.AssertSuccess(t)
// Verify record exists
suite.AssertRecordExists(t, &User{}, "name = ?", "Test")
// Transaction rolls back after test
}Suite Helper Methods
// Check if record exists
suite.AssertRecordExists(t, &User{}, "email = ?", "john@example.com")
// Check if record does NOT exist
suite.AssertRecordNotExists(t, &User{}, "email = ?", "deleted@example.com")
// Check record count
suite.AssertRecordCount(t, &User{}, 5) // Expect 5 users
suite.AssertRecordCount(t, &User{}, 2, "active = ?", true) // Expect 2 active users
// Use authenticated client
authClient := suite.WithAuth("admin-token")
resp := authClient.GET("/admin/users")FixtureManager
Manage test data in the database:
fixtures := echonexttest.NewFixtureManager(db)
// Load individual records
err := fixtures.Load(
&User{Name: "John", Email: "john@example.com"},
&User{Name: "Jane", Email: "jane@example.com"},
)
// Load multiple records of same type
users := []User{
{Name: "User 1"},
{Name: "User 2"},
}
err := fixtures.LoadMany(&users)
// Clear all loaded fixtures
err := fixtures.Clear()
// Clear specific table
err := fixtures.ClearTable("users")
// Clear ALL tables (use with caution)
err := fixtures.ClearAll()Retrieving Fixtures
// Get loaded fixture by table and index
user, err := fixtures.Get("users", 0)
// Get count of loaded fixtures
count := fixtures.Count("users")Factory Pattern
Use factories for creating test entities with sensible defaults:
import echonexttest "github.com/abdussamadbello/echonext/pkg/contrib/testing"
// Create a factory
userFactory := echonexttest.NewFactory(db, func() User {
return User{
Name: "Default Name",
Email: fmt.Sprintf("user-%d@example.com", time.Now().UnixNano()),
Active: true,
}
})
// Create and persist entity
user, err := userFactory.Create()
// Create multiple entities
users, err := userFactory.CreateMany(5)
// Build without persisting
user := userFactory.Build()
users := userFactory.BuildMany(10)Factory with Overrides
// Override specific fields
user, err := userFactory.
With("Name", "Custom Name").
With("Active", false).
Create()Complete Example
Here's a complete test file demonstrating all features:
package user_test
import (
"testing"
"github.com/abdussamadbello/echonext"
echonexttest "github.com/abdussamadbello/echonext/pkg/contrib/testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name"`
Email string `json:"email"`
}
type CreateUserRequest struct {
Name string `json:"name" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
func setupTestApp() (*echonext.App, *gorm.DB) {
app := echonext.New()
db := setupTestDB()
// Register routes
app.GET("/users", func(c echo.Context) ([]User, error) {
var users []User
db.Find(&users)
return users, nil
})
app.POST("/users", func(c echo.Context, req CreateUserRequest) (User, error) {
user := User{Name: req.Name, Email: req.Email}
db.Create(&user)
return user, nil
})
return app, db
}
func TestListUsers(t *testing.T) {
app, db := setupTestApp()
client := echonexttest.NewAPIClient(app)
fixtures := echonexttest.NewFixtureManager(db)
// Setup
fixtures.Load(
&User{Name: "John", Email: "john@example.com"},
&User{Name: "Jane", Email: "jane@example.com"},
)
defer fixtures.Clear()
// Test
resp := client.GET("/users")
// Assert
resp.AssertStatus(t, 200)
resp.AssertSuccess(t)
var result echonext.Response[[]User]
require.NoError(t, resp.JSON(&result))
assert.Len(t, result.Data, 2)
}
func TestCreateUser(t *testing.T) {
app, db := setupTestApp()
suite := echonexttest.NewIntegrationSuite(app, db)
suite.BeginTx()
defer suite.RollbackTx()
// Test
resp := suite.Client.POST("/users", CreateUserRequest{
Name: "New User",
Email: "new@example.com",
})
// Assert
resp.AssertStatus(t, 200)
suite.AssertRecordExists(t, &User{}, "email = ?", "new@example.com")
}
func TestCreateUserValidation(t *testing.T) {
app, _ := setupTestApp()
client := echonexttest.NewAPIClient(app)
// Test with missing required fields
resp := client.POST("/users", map[string]string{
"name": "", // Empty name
})
// Assert
resp.AssertStatus(t, 400)
resp.AssertError(t)
}
func TestAuthenticatedEndpoint(t *testing.T) {
app, _ := setupTestApp()
// Test without auth
client := echonexttest.NewAPIClient(app)
resp := client.GET("/admin/users")
resp.AssertStatus(t, 401)
// Test with auth
authClient := client.WithAuth("valid-token")
resp = authClient.GET("/admin/users")
resp.AssertStatus(t, 200)
}
func TestUserFactory(t *testing.T) {
_, db := setupTestApp()
factory := echonexttest.NewFactory(db, func() User {
return User{
Name: "Factory User",
Email: fmt.Sprintf("factory-%d@example.com", time.Now().UnixNano()),
}
})
// Create 10 test users
users, err := factory.CreateMany(10)
require.NoError(t, err)
assert.Len(t, users, 10)
// Clean up
for _, u := range users {
db.Delete(u)
}
}Best Practices
Test Organization
- Use suites for related tests that share setup
- Use IntegrationSuite for database tests to ensure isolation
- Clear fixtures in teardown to prevent test pollution
Assertions
- Prefer built-in assertions (
AssertStatus,AssertSuccess) for clarity - Chain assertions where it makes sense
- Use
requirefor critical assertions that should stop the test
Fixtures
- Create minimal fixtures - only what's needed for the test
- Use factories for complex entities with many fields
- Always clean up fixtures after tests
API Testing
- Test both success and error cases
- Test validation with invalid inputs
- Test authentication for protected endpoints
- Verify response bodies not just status codes