From dc235ba531a51e1f7d7699d31f59a382c253d0a9 Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Mon, 13 Jan 2025 18:59:27 +0100 Subject: [PATCH 01/16] feat: add framework-agnostic mock context for testing Implements a MockContext[B] type that allows testing controllers without framework dependencies. Includes comprehensive tests and documentation. --- README.md | 47 +++++++++++++++++++ mock_context.go | 105 +++++++++++++++++++++++++++++++++++++++++++ mock_context_test.go | 74 ++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 mock_context.go create mode 100644 mock_context_test.go diff --git a/README.md b/README.md index 6d1984a5..cea13b1e 100644 --- a/README.md +++ b/README.md @@ -363,3 +363,50 @@ Go instead of Node, I'm happy to use it. [gin-gonic-issue]: https://github.com/gin-gonic/gin/issues/155 [contributors-url]: https://github.com/go-fuego/fuego/graphs/contributors + +## Testing with Mock Context + +Fuego provides a framework-agnostic mock context for testing your controllers. This allows you to test your business logic without depending on specific web frameworks. + +### Basic Usage + +```go +func TestMyController(t *testing.T) { + // Create a mock context with your request body type + ctx := fuego.NewMockContext[UserRequest]() + + // Set up test data + ctx.SetBody(UserRequest{ + Name: "John Doe", + Email: "john@example.com", + }) + + // Add query parameters if needed + ctx.SetURLValues(url.Values{ + "filter": []string{"active"}, + }) + + // Add path parameters + ctx.SetPathParam("id", "123") + + // Call your controller + result, err := MyController(ctx) + + // Assert results + assert.NoError(t, err) + assert.Equal(t, expectedResult, result) +} +``` + +### Features + +The mock context supports: + +- Type-safe request bodies with generics +- URL query parameters +- Path parameters +- Headers +- Custom context values +- Request/Response objects + +This makes it easy to test your controllers without worrying about HTTP mechanics or framework specifics. diff --git a/mock_context.go b/mock_context.go new file mode 100644 index 00000000..4b89fe65 --- /dev/null +++ b/mock_context.go @@ -0,0 +1,105 @@ +package fuego + +import ( + "context" + "net/http" + "net/url" +) + +// MockContext provides a framework-agnostic implementation of ContextWithBody +// for testing purposes. It allows testing controllers without depending on +// specific web frameworks like Gin or Echo. +type MockContext[B any] struct { + body B + urlValues url.Values + headers http.Header + pathParams map[string]string + ctx context.Context + response http.ResponseWriter + request *http.Request +} + +// NewMockContext creates a new MockContext instance with initialized maps +// for URL values, headers, and path parameters. It uses context.Background() +// as the default context. +func NewMockContext[B any]() *MockContext[B] { + return &MockContext[B]{ + urlValues: make(url.Values), + headers: make(http.Header), + pathParams: make(map[string]string), + ctx: context.Background(), + } +} + +// Body returns the previously set body value. This method always returns +// nil as the error value, as the mock context doesn't perform actual +// deserialization. +func (m *MockContext[B]) Body() (B, error) { + return m.body, nil +} + +// SetBody stores the provided body value for later retrieval via Body(). +// This is typically used in tests to simulate request bodies. +func (m *MockContext[B]) SetBody(body B) { + m.body = body +} + +// URLValues returns the mock URL values +func (m *MockContext[B]) URLValues() url.Values { + return m.urlValues +} + +// SetURLValues sets the mock URL values +func (m *MockContext[B]) SetURLValues(values url.Values) { + m.urlValues = values +} + +// Header returns the mock headers +func (m *MockContext[B]) Header() http.Header { + return m.headers +} + +// SetHeader sets a mock header +func (m *MockContext[B]) SetHeader(key, value string) { + m.headers.Set(key, value) +} + +// PathParam returns a mock path parameter +func (m *MockContext[B]) PathParam(name string) string { + return m.pathParams[name] +} + +// SetPathParam sets a mock path parameter +func (m *MockContext[B]) SetPathParam(name, value string) { + m.pathParams[name] = value +} + +// Context returns the mock context +func (m *MockContext[B]) Context() context.Context { + return m.ctx +} + +// SetContext sets the mock context +func (m *MockContext[B]) SetContext(ctx context.Context) { + m.ctx = ctx +} + +// Response returns the mock response writer +func (m *MockContext[B]) Response() http.ResponseWriter { + return m.response +} + +// SetResponse sets the mock response writer +func (m *MockContext[B]) SetResponse(w http.ResponseWriter) { + m.response = w +} + +// Request returns the mock request +func (m *MockContext[B]) Request() *http.Request { + return m.request +} + +// SetRequest sets the mock request +func (m *MockContext[B]) SetRequest(r *http.Request) { + m.request = r +} \ No newline at end of file diff --git a/mock_context_test.go b/mock_context_test.go new file mode 100644 index 00000000..8118ba3a --- /dev/null +++ b/mock_context_test.go @@ -0,0 +1,74 @@ +package fuego + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestBody struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func TestMockContext(t *testing.T) { + // Create a new mock context + ctx := NewMockContext[TestBody]() + + // Test body + body := TestBody{ + Name: "John", + Age: 30, + } + ctx.SetBody(body) + gotBody, err := ctx.Body() + assert.NoError(t, err) + assert.Equal(t, body, gotBody) + + // Test URL values + values := url.Values{ + "key": []string{"value"}, + } + ctx.SetURLValues(values) + assert.Equal(t, values, ctx.URLValues()) + + // Test headers + ctx.SetHeader("Content-Type", "application/json") + assert.Equal(t, "application/json", ctx.Header().Get("Content-Type")) + + // Test path params + ctx.SetPathParam("id", "123") + assert.Equal(t, "123", ctx.PathParam("id")) +} + +func TestMockContextAdvanced(t *testing.T) { + // Test with custom context + ctx := NewMockContext[TestBody]() + customCtx := context.WithValue(context.Background(), "key", "value") + ctx.SetContext(customCtx) + assert.Equal(t, "value", ctx.Context().Value("key")) + + // Test with request/response + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/test", nil) + ctx.SetResponse(w) + ctx.SetRequest(r) + assert.Equal(t, w, ctx.Response()) + assert.Equal(t, r, ctx.Request()) + + // Test multiple headers + ctx.SetHeader("X-Test-1", "value1") + ctx.SetHeader("X-Test-2", "value2") + assert.Equal(t, "value1", ctx.Header().Get("X-Test-1")) + assert.Equal(t, "value2", ctx.Header().Get("X-Test-2")) + + // Test multiple path params + ctx.SetPathParam("id", "123") + ctx.SetPathParam("category", "books") + assert.Equal(t, "123", ctx.PathParam("id")) + assert.Equal(t, "books", ctx.PathParam("category")) +} \ No newline at end of file From 82afb5ac5b1ff1743f66c4e8be7cb1d8a89c4cfb Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Mon, 13 Jan 2025 20:05:33 +0100 Subject: [PATCH 02/16] feat(test): add framework-agnostic mock context Add MockContext[B] to help users test their controllers without framework dependencies. Includes realistic examples and documentation. - Move testing docs from README.md to TESTING.md - Add mock context with getters/setters - Add example user controller tests - Use fuego_test package for end-user perspective Closes #349 --- README.md | 51 +---------- TESTING.md | 195 +++++++++++++++++++++++++++++++++++++++ mock_context.go | 2 +- mock_context_test.go | 214 +++++++++++++++++++++++++++++++------------ 4 files changed, 357 insertions(+), 105 deletions(-) create mode 100644 TESTING.md diff --git a/README.md b/README.md index cea13b1e..07e6dc6b 100644 --- a/README.md +++ b/README.md @@ -361,52 +361,9 @@ Go instead of Node, I'm happy to use it. [MIT](./LICENSE.txt) -[gin-gonic-issue]: https://github.com/gin-gonic/gin/issues/155 -[contributors-url]: https://github.com/go-fuego/fuego/graphs/contributors - -## Testing with Mock Context - -Fuego provides a framework-agnostic mock context for testing your controllers. This allows you to test your business logic without depending on specific web frameworks. +## Testing -### Basic Usage +For information about testing your Fuego applications, please see [TESTING.md](TESTING.md). -```go -func TestMyController(t *testing.T) { - // Create a mock context with your request body type - ctx := fuego.NewMockContext[UserRequest]() - - // Set up test data - ctx.SetBody(UserRequest{ - Name: "John Doe", - Email: "john@example.com", - }) - - // Add query parameters if needed - ctx.SetURLValues(url.Values{ - "filter": []string{"active"}, - }) - - // Add path parameters - ctx.SetPathParam("id", "123") - - // Call your controller - result, err := MyController(ctx) - - // Assert results - assert.NoError(t, err) - assert.Equal(t, expectedResult, result) -} -``` - -### Features - -The mock context supports: - -- Type-safe request bodies with generics -- URL query parameters -- Path parameters -- Headers -- Custom context values -- Request/Response objects - -This makes it easy to test your controllers without worrying about HTTP mechanics or framework specifics. +[gin-gonic-issue]: https://github.com/gin-gonic/gin/issues/155 +[contributors-url]: https://github.com/go-fuego/fuego/graphs/contributors diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..27bd6f4b --- /dev/null +++ b/TESTING.md @@ -0,0 +1,195 @@ +# Testing in Fuego + +This guide explains how to effectively test your Fuego applications, with a focus on using the mock context for testing controllers. + +## Mock Context + +Fuego provides a framework-agnostic mock context that allows you to test your controllers without depending on specific web frameworks. This makes it easy to focus on testing your business logic rather than HTTP mechanics. + +### Basic Usage + +```go +func TestMyController(t *testing.T) { + // Create a mock context with your request body type + ctx := fuego.NewMockContext[UserRequest]() + + // Set up test data + ctx.SetBody(UserRequest{ + Name: "John Doe", + Email: "john@example.com", + }) + + // Add query parameters if needed + ctx.SetURLValues(url.Values{ + "filter": []string{"active"}, + }) + + // Add path parameters + ctx.SetPathParam("id", "123") + + // Call your controller + result, err := MyController(ctx) + + // Assert results + assert.NoError(t, err) + assert.Equal(t, expectedResult, result) +} +``` + +### Features + +The mock context supports: + +- Type-safe request bodies with generics +- URL query parameters +- Path parameters +- Headers +- Custom context values +- Request/Response objects + +### Advanced Usage + +#### Testing with Headers + +```go +func TestControllerWithHeaders(t *testing.T) { + ctx := fuego.NewMockContext[EmptyBody]() + ctx.SetHeader("Authorization", "Bearer token123") + ctx.SetHeader("Content-Type", "application/json") + + result, err := MyAuthenticatedController(ctx) + assert.NoError(t, err) +} +``` + +#### Testing with Custom Context Values + +```go +func TestControllerWithContext(t *testing.T) { + ctx := fuego.NewMockContext[EmptyBody]() + customCtx := context.WithValue(context.Background(), "user_id", "123") + ctx.SetContext(customCtx) + + result, err := MyContextAwareController(ctx) + assert.NoError(t, err) +} +``` + +#### Testing with Request/Response Objects + +```go +func TestControllerWithRequestResponse(t *testing.T) { + ctx := fuego.NewMockContext[EmptyBody]() + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/test", nil) + + ctx.SetResponse(w) + ctx.SetRequest(r) + + result, err := MyController(ctx) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, w.Code) +} +``` + +### Best Practices + +1. **Use Table-Driven Tests** + +```go +func TestUserController(t *testing.T) { + tests := []struct { + name string + body UserRequest + want UserResponse + wantErr bool + }{ + { + name: "valid user", + body: UserRequest{Name: "John", Email: "john@example.com"}, + want: UserResponse{ID: "123", Name: "John"}, + }, + { + name: "invalid email", + body: UserRequest{Name: "John", Email: "invalid"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := fuego.NewMockContext[UserRequest]() + ctx.SetBody(tt.body) + + got, err := CreateUser(ctx) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} +``` + +2. **Test Error Cases** + +```go +func TestErrorHandling(t *testing.T) { + ctx := fuego.NewMockContext[UserRequest]() + ctx.SetBody(UserRequest{}) // Empty body should trigger validation error + + _, err := CreateUser(ctx) + assert.Error(t, err) + assert.Contains(t, err.Error(), "validation failed") +} +``` + +3. **Test Validation Rules** + +```go +func TestValidation(t *testing.T) { + ctx := fuego.NewMockContext[UserRequest]() + ctx.SetBody(UserRequest{ + Name: "", // Required field + Email: "invalid-email", // Invalid format + }) + + _, err := CreateUser(ctx) + assert.Error(t, err) +} +``` + +4. **Test Middleware Integration** + +```go +func TestWithMiddleware(t *testing.T) { + ctx := fuego.NewMockContext[EmptyBody]() + ctx.SetHeader("Authorization", "Bearer valid-token") + + // Test that middleware allows the request + result, err := AuthMiddleware(MyProtectedController)(ctx) + assert.NoError(t, err) + + // Test that middleware blocks unauthorized requests + ctx.SetHeader("Authorization", "invalid-token") + _, err = AuthMiddleware(MyProtectedController)(ctx) + assert.Error(t, err) +} +``` + +### Tips for Effective Testing + +1. Keep tests focused on business logic +2. Use meaningful test names that describe the scenario +3. Test both success and failure cases +4. Use helper functions for common test setup +5. Test validation rules thoroughly +6. Mock external dependencies when needed +7. Use subtests for better organization +8. Test edge cases and boundary conditions + +## Contributing + +If you find any issues or have suggestions for improving the testing utilities, please open an issue or submit a pull request. diff --git a/mock_context.go b/mock_context.go index 4b89fe65..db4a304a 100644 --- a/mock_context.go +++ b/mock_context.go @@ -102,4 +102,4 @@ func (m *MockContext[B]) Request() *http.Request { // SetRequest sets the mock request func (m *MockContext[B]) SetRequest(r *http.Request) { m.request = r -} \ No newline at end of file +} diff --git a/mock_context_test.go b/mock_context_test.go index 8118ba3a..182f2234 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -1,74 +1,174 @@ -package fuego +package fuego_test import ( - "context" - "net/http" - "net/http/httptest" - "net/url" + "errors" "testing" + "github.com/go-fuego/fuego" "github.com/stretchr/testify/assert" ) -type TestBody struct { - Name string `json:"name"` - Age int `json:"age"` +// UserRequest represents the incoming request body +type UserRequest struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=8"` } -func TestMockContext(t *testing.T) { - // Create a new mock context - ctx := NewMockContext[TestBody]() +// UserResponse represents the API response +type UserResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +// CreateUserController is a typical controller that creates a user +func CreateUserController(c fuego.ContextWithBody[UserRequest]) (UserResponse, error) { + // Get and validate the request body + body, err := c.Body() + if err != nil { + return UserResponse{}, err + } - // Test body - body := TestBody{ - Name: "John", - Age: 30, + // Check if email is already taken (simulating DB check) + if body.Email == "taken@example.com" { + return UserResponse{}, errors.New("email already taken") } - ctx.SetBody(body) - gotBody, err := ctx.Body() - assert.NoError(t, err) - assert.Equal(t, body, gotBody) - - // Test URL values - values := url.Values{ - "key": []string{"value"}, + + // In a real app, you would: + // 1. Hash the password + // 2. Save to database + // 3. Generate ID + // Here we'll simulate that: + user := UserResponse{ + ID: "user_123", // Simulated generated ID + Name: body.Name, + Email: body.Email, + } + + return user, nil +} + +func TestCreateUserController(t *testing.T) { + tests := []struct { + name string + request UserRequest + want UserResponse + wantErr string + }{ + { + name: "successful creation", + request: UserRequest{ + Name: "John Doe", + Email: "john@example.com", + Password: "secure123", + }, + want: UserResponse{ + ID: "user_123", + Name: "John Doe", + Email: "john@example.com", + }, + }, + { + name: "email already taken", + request: UserRequest{ + Name: "Jane Doe", + Email: "taken@example.com", + Password: "secure123", + }, + wantErr: "email already taken", + }, } - ctx.SetURLValues(values) - assert.Equal(t, values, ctx.URLValues()) - // Test headers - ctx.SetHeader("Content-Type", "application/json") - assert.Equal(t, "application/json", ctx.Header().Get("Content-Type")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup + ctx := fuego.NewMockContext[UserRequest]() + ctx.SetBody(tt.request) + + // Execute + got, err := CreateUserController(ctx) - // Test path params - ctx.SetPathParam("id", "123") - assert.Equal(t, "123", ctx.PathParam("id")) + // Assert + if tt.wantErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } } -func TestMockContextAdvanced(t *testing.T) { - // Test with custom context - ctx := NewMockContext[TestBody]() - customCtx := context.WithValue(context.Background(), "key", "value") - ctx.SetContext(customCtx) - assert.Equal(t, "value", ctx.Context().Value("key")) - - // Test with request/response - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/test", nil) - ctx.SetResponse(w) - ctx.SetRequest(r) - assert.Equal(t, w, ctx.Response()) - assert.Equal(t, r, ctx.Request()) - - // Test multiple headers - ctx.SetHeader("X-Test-1", "value1") - ctx.SetHeader("X-Test-2", "value2") - assert.Equal(t, "value1", ctx.Header().Get("X-Test-1")) - assert.Equal(t, "value2", ctx.Header().Get("X-Test-2")) - - // Test multiple path params - ctx.SetPathParam("id", "123") - ctx.SetPathParam("category", "books") - assert.Equal(t, "123", ctx.PathParam("id")) - assert.Equal(t, "books", ctx.PathParam("category")) +// Example of testing a controller that uses path parameters +func GetUserController(c fuego.ContextNoBody) (UserResponse, error) { + userID := c.PathParam("id") + if userID == "" { + return UserResponse{}, errors.New("user ID is required") + } + + // Simulate fetching user from database + if userID == "not_found" { + return UserResponse{}, errors.New("user not found") + } + + return UserResponse{ + ID: userID, + Name: "John Doe", + Email: "john@example.com", + }, nil +} + +func TestGetUserController(t *testing.T) { + tests := []struct { + name string + userID string + want UserResponse + wantErr string + }{ + { + name: "user found", + userID: "user_123", + want: UserResponse{ + ID: "user_123", + Name: "John Doe", + Email: "john@example.com", + }, + }, + { + name: "user not found", + userID: "not_found", + wantErr: "user not found", + }, + { + name: "missing user ID", + userID: "", + wantErr: "user ID is required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup + ctx := fuego.NewMockContext[struct{}]() + if tt.userID != "" { + ctx.SetPathParam("id", tt.userID) + } + + // Execute + got, err := GetUserController(ctx) + + // Assert + if tt.wantErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } } \ No newline at end of file From 94d151ce57706bda6249f628d2204835fdf7ad68 Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Mon, 13 Jan 2025 21:09:08 +0100 Subject: [PATCH 03/16] feat(test): add realistic mock context example Replace basic getter/setter tests with a real-world example: - Add UserController with Create and GetByID endpoints - Add UserService with mock implementation - Show proper separation of concerns - Demonstrate practical testing patterns - Move testing docs from README.md to TESTING.md This provides users with a clear example of how to test their controllers using the mock context in a real-world scenario. Closes #349 --- mock_context.go | 235 ++++++++++++++++++++++++++++++- mock_context_test.go | 322 +++++++++++++++++++++++++------------------ 2 files changed, 417 insertions(+), 140 deletions(-) diff --git a/mock_context.go b/mock_context.go index db4a304a..3c12d0e5 100644 --- a/mock_context.go +++ b/mock_context.go @@ -2,8 +2,14 @@ package fuego import ( "context" + "fmt" + "io" "net/http" "net/url" + "strconv" + "time" + + "github.com/go-fuego/fuego/internal" ) // MockContext provides a framework-agnostic implementation of ContextWithBody @@ -17,6 +23,7 @@ type MockContext[B any] struct { ctx context.Context response http.ResponseWriter request *http.Request + cookies map[string]*http.Cookie } // NewMockContext creates a new MockContext instance with initialized maps @@ -28,6 +35,7 @@ func NewMockContext[B any]() *MockContext[B] { headers: make(http.Header), pathParams: make(map[string]string), ctx: context.Background(), + cookies: make(map[string]*http.Cookie), } } @@ -54,9 +62,9 @@ func (m *MockContext[B]) SetURLValues(values url.Values) { m.urlValues = values } -// Header returns the mock headers -func (m *MockContext[B]) Header() http.Header { - return m.headers +// Header returns the value of the header with the given key +func (m *MockContext[B]) Header(key string) string { + return m.headers.Get(key) } // SetHeader sets a mock header @@ -64,6 +72,11 @@ func (m *MockContext[B]) SetHeader(key, value string) { m.headers.Set(key, value) } +// GetHeaders returns all headers (helper method for testing) +func (m *MockContext[B]) GetHeaders() http.Header { + return m.headers +} + // PathParam returns a mock path parameter func (m *MockContext[B]) PathParam(name string) string { return m.pathParams[name] @@ -103,3 +116,219 @@ func (m *MockContext[B]) Request() *http.Request { func (m *MockContext[B]) SetRequest(r *http.Request) { m.request = r } + +// Cookie returns a cookie by name +func (m *MockContext[B]) Cookie(name string) (*http.Cookie, error) { + if cookie, exists := m.cookies[name]; exists { + return cookie, nil + } + return nil, http.ErrNoCookie +} + +// SetCookie sets a cookie for testing +func (m *MockContext[B]) SetCookie(cookie http.Cookie) { + m.cookies[cookie.Name] = &cookie +} + +// Deadline returns the time when work done on behalf of this context +// should be canceled. In this mock implementation, we return no deadline. +func (m *MockContext[B]) Deadline() (deadline time.Time, ok bool) { + return time.Time{}, false +} + +// Done returns a channel that's closed when work done on behalf of this +// context should be canceled. In this mock implementation, we return nil +// which means the context can never be canceled. +func (m *MockContext[B]) Done() <-chan struct{} { + return nil +} + +// Err returns nil since this mock context never returns errors +func (m *MockContext[B]) Err() error { + return nil +} + +// EmptyBody represents an empty request body +type EmptyBody struct{} + +// GetOpenAPIParams returns an empty map since this is just a mock +func (m *MockContext[B]) GetOpenAPIParams() map[string]internal.OpenAPIParam { + return make(map[string]internal.OpenAPIParam) +} + +// HasCookie checks if a cookie with the given name exists +func (m *MockContext[B]) HasCookie(name string) bool { + _, exists := m.cookies[name] + return exists +} + +// HasHeader checks if a header with the given key exists +func (m *MockContext[B]) HasHeader(key string) bool { + _, exists := m.headers[key] + return exists +} + +// HasQueryParam checks if a query parameter with the given key exists +func (m *MockContext[B]) HasQueryParam(key string) bool { + _, exists := m.urlValues[key] + return exists +} + +// MainLang returns the main language for the request (e.g., "en"). +// In this mock implementation, we'll return "en" as default. +func (m *MockContext[B]) MainLang() string { + // Get language from Accept-Language header or return default + if lang := m.headers.Get("Accept-Language"); lang != "" { + return lang[:2] // Take first two chars for language code + } + return "en" // Default to English +} + +// MainLocale returns the main locale for the request (e.g., "en-US"). +// In this mock implementation, we'll return "en-US" as default. +func (m *MockContext[B]) MainLocale() string { + // Get locale from Accept-Language header or return default + if locale := m.headers.Get("Accept-Language"); locale != "" { + return locale + } + return "en-US" // Default to English (US) +} + +// MustBody returns the body directly, without error handling. +// In this mock implementation, we simply return the body since we know it's valid. +func (m *MockContext[B]) MustBody() B { + return m.body +} + +// QueryParam returns the value of the query parameter with the given key. +// If there are multiple values, it returns the first one. +// If the parameter doesn't exist, it returns an empty string. +func (m *MockContext[B]) QueryParam(key string) string { + return m.urlValues.Get(key) +} + +// QueryParamArr returns all values for the query parameter with the given key. +// If the parameter doesn't exist, it returns an empty slice. +func (m *MockContext[B]) QueryParamArr(key string) []string { + return m.urlValues[key] +} + +// QueryParamBool returns the boolean value of the query parameter with the given key. +// Returns true for "1", "t", "T", "true", "TRUE", "True" +// Returns false for "0", "f", "F", "false", "FALSE", "False" +// Returns false for any other value +func (m *MockContext[B]) QueryParamBool(key string) bool { + v := m.urlValues.Get(key) + switch v { + case "1", "t", "T", "true", "TRUE", "True": + return true + default: + return false + } +} + +// QueryParamBoolErr returns the boolean value of the query parameter with the given key +// and an error if the value is not a valid boolean. +func (m *MockContext[B]) QueryParamBoolErr(key string) (bool, error) { + v := m.urlValues.Get(key) + switch v { + case "1", "t", "T", "true", "TRUE", "True": + return true, nil + case "0", "f", "F", "false", "FALSE", "False": + return false, nil + case "": + return false, nil // Parameter not found + default: + return false, fmt.Errorf("invalid boolean value: %s", v) + } +} + +// QueryParamInt returns the integer value of the query parameter with the given key. +// Returns 0 if the parameter doesn't exist or cannot be parsed as an integer. +func (m *MockContext[B]) QueryParamInt(key string) int { + v := m.urlValues.Get(key) + if v == "" { + return 0 + } + i, err := strconv.Atoi(v) + if err != nil { + return 0 + } + return i +} + +// QueryParamIntErr returns the integer value of the query parameter with the given key +// and an error if the value cannot be parsed as an integer. +func (m *MockContext[B]) QueryParamIntErr(key string) (int, error) { + v := m.urlValues.Get(key) + if v == "" { + return 0, nil // Parameter not found + } + i, err := strconv.Atoi(v) + if err != nil { + return 0, fmt.Errorf("invalid integer value: %s", v) + } + return i, nil +} + +// QueryParams returns all query parameters. +// This is an alias for URLValues() for compatibility with the interface. +func (m *MockContext[B]) QueryParams() url.Values { + return m.urlValues +} + +// Redirect performs a redirect to the specified URL with the given status code. +// In this mock implementation, we store the redirect information for testing. +func (m *MockContext[B]) Redirect(code int, url string) (any, error) { + if m.response != nil { + m.response.Header().Set("Location", url) + m.response.WriteHeader(code) + } + return nil, nil +} + +// mockRenderer implements CtxRenderer for testing +type mockRenderer struct { + data any + template string + layouts []string +} + +// Render implements the CtxRenderer interface +func (r *mockRenderer) Render(ctx context.Context, w io.Writer) error { + // In a real implementation, this would render the template + // For testing, we just write a success status + if hw, ok := w.(http.ResponseWriter); ok { + hw.WriteHeader(http.StatusOK) + } + return nil +} + +// Render renders the template with the given name and data. +// In this mock implementation, we just store the data for testing. +func (m *MockContext[B]) Render(templateName string, data any, layouts ...string) (CtxRenderer, error) { + if m.response != nil { + // In a real implementation, this would render the template with layouts + // For testing, we just store the data + } + return &mockRenderer{ + data: data, + template: templateName, + layouts: layouts, + }, nil +} + +// SetStatus sets the HTTP status code for the response. +// In this mock implementation, we set the status code if a response writer is available. +func (m *MockContext[B]) SetStatus(code int) { + if m.response != nil { + m.response.WriteHeader(code) + } +} + +// Value returns the value associated with this context for key, or nil +// if no value is associated with key. In this mock implementation, +// we delegate to the underlying context. +func (m *MockContext[B]) Value(key any) any { + return m.ctx.Value(key) +} diff --git a/mock_context_test.go b/mock_context_test.go index 182f2234..a66b01aa 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -2,173 +2,221 @@ package fuego_test import ( "errors" + "net/http" + "net/http/httptest" "testing" "github.com/go-fuego/fuego" "github.com/stretchr/testify/assert" ) -// UserRequest represents the incoming request body -type UserRequest struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required,min=8"` -} - -// UserResponse represents the API response -type UserResponse struct { +// UserProfile represents a user in our system +type UserProfile struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` } -// CreateUserController is a typical controller that creates a user -func CreateUserController(c fuego.ContextWithBody[UserRequest]) (UserResponse, error) { - // Get and validate the request body - body, err := c.Body() - if err != nil { - return UserResponse{}, err - } +// UserService simulates a real service layer +type UserService interface { + CreateUser(name, email string) (UserProfile, error) + GetUserByID(id string) (UserProfile, error) +} - // Check if email is already taken (simulating DB check) - if body.Email == "taken@example.com" { - return UserResponse{}, errors.New("email already taken") - } +// mockUserService is a mock implementation of UserService +type mockUserService struct { + users map[string]UserProfile +} - // In a real app, you would: - // 1. Hash the password - // 2. Save to database - // 3. Generate ID - // Here we'll simulate that: - user := UserResponse{ - ID: "user_123", // Simulated generated ID - Name: body.Name, - Email: body.Email, +func newMockUserService() *mockUserService { + return &mockUserService{ + users: map[string]UserProfile{ + "123": {ID: "123", Name: "John Doe", Email: "john@example.com"}, + }, } +} +func (s *mockUserService) CreateUser(name, email string) (UserProfile, error) { + if email == "taken@example.com" { + return UserProfile{}, errors.New("email already taken") + } + user := UserProfile{ + ID: "new_id", + Name: name, + Email: email, + } + s.users[user.ID] = user return user, nil } -func TestCreateUserController(t *testing.T) { - tests := []struct { - name string - request UserRequest - want UserResponse - wantErr string - }{ - { - name: "successful creation", - request: UserRequest{ - Name: "John Doe", - Email: "john@example.com", - Password: "secure123", - }, - want: UserResponse{ - ID: "user_123", - Name: "John Doe", - Email: "john@example.com", - }, - }, - { - name: "email already taken", - request: UserRequest{ - Name: "Jane Doe", - Email: "taken@example.com", - Password: "secure123", - }, - wantErr: "email already taken", - }, +func (s *mockUserService) GetUserByID(id string) (UserProfile, error) { + user, exists := s.users[id] + if !exists { + return UserProfile{}, errors.New("user not found") } + return user, nil +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup - ctx := fuego.NewMockContext[UserRequest]() - ctx.SetBody(tt.request) - - // Execute - got, err := CreateUserController(ctx) - - // Assert - if tt.wantErr != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr) - return - } - - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } +// CreateUserRequest represents the request body for user creation +type CreateUserRequest struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` +} + +// UserController handles user-related HTTP endpoints +type UserController struct { + service UserService } -// Example of testing a controller that uses path parameters -func GetUserController(c fuego.ContextNoBody) (UserResponse, error) { - userID := c.PathParam("id") - if userID == "" { - return UserResponse{}, errors.New("user ID is required") +func NewUserController(service UserService) *UserController { + return &UserController{service: service} +} + +// Create handles user creation +func (c *UserController) Create(ctx fuego.ContextWithBody[CreateUserRequest]) (UserProfile, error) { + req, err := ctx.Body() + if err != nil { + return UserProfile{}, err } - // Simulate fetching user from database - if userID == "not_found" { - return UserResponse{}, errors.New("user not found") + user, err := c.service.CreateUser(req.Name, req.Email) + if err != nil { + return UserProfile{}, err } - return UserResponse{ - ID: userID, - Name: "John Doe", - Email: "john@example.com", - }, nil + ctx.SetStatus(http.StatusCreated) + return user, nil } -func TestGetUserController(t *testing.T) { - tests := []struct { - name string - userID string - want UserResponse - wantErr string - }{ - { - name: "user found", - userID: "user_123", - want: UserResponse{ - ID: "user_123", - Name: "John Doe", - Email: "john@example.com", - }, - }, - { - name: "user not found", - userID: "not_found", - wantErr: "user not found", - }, - { - name: "missing user ID", - userID: "", - wantErr: "user ID is required", - }, +// GetByID handles fetching a user by ID +func (c *UserController) GetByID(ctx fuego.ContextWithBody[any]) (UserProfile, error) { + id := ctx.PathParam("id") + if id == "" { + return UserProfile{}, errors.New("id is required") } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup - ctx := fuego.NewMockContext[struct{}]() - if tt.userID != "" { - ctx.SetPathParam("id", tt.userID) - } + user, err := c.service.GetUserByID(id) + if err != nil { + if err.Error() == "user not found" { + ctx.SetStatus(http.StatusNotFound) + } + return UserProfile{}, err + } - // Execute - got, err := GetUserController(ctx) + return user, nil +} - // Assert - if tt.wantErr != "" { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.wantErr) - return - } +func TestUserController(t *testing.T) { + // Setup + service := newMockUserService() + controller := NewUserController(service) + + t.Run("create user", func(t *testing.T) { + tests := []struct { + name string + request CreateUserRequest + want UserProfile + wantErr string + status int + }{ + { + name: "successful creation", + request: CreateUserRequest{ + Name: "Jane Doe", + Email: "jane@example.com", + }, + want: UserProfile{ + ID: "new_id", + Name: "Jane Doe", + Email: "jane@example.com", + }, + status: http.StatusCreated, + }, + { + name: "email taken", + request: CreateUserRequest{ + Name: "Another User", + Email: "taken@example.com", + }, + wantErr: "email already taken", + status: http.StatusOK, // Default status when not set + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup mock context + w := httptest.NewRecorder() + ctx := fuego.NewMockContext[CreateUserRequest]() + ctx.SetBody(tt.request) + ctx.SetResponse(w) + + // Call controller + got, err := controller.Create(ctx) + + // Assert results + if tt.wantErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.status, w.Code) + }) + } + }) + + t.Run("get user by id", func(t *testing.T) { + tests := []struct { + name string + userID string + want UserProfile + wantErr string + status int + }{ + { + name: "user found", + userID: "123", + want: UserProfile{ + ID: "123", + Name: "John Doe", + Email: "john@example.com", + }, + status: http.StatusOK, + }, + { + name: "user not found", + userID: "999", + wantErr: "user not found", + status: http.StatusNotFound, + }, + } - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} \ No newline at end of file + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup mock context + w := httptest.NewRecorder() + ctx := fuego.NewMockContext[any]() + ctx.SetPathParam("id", tt.userID) + ctx.SetResponse(w) + + // Call controller + got, err := controller.GetByID(ctx) + + // Assert results + if tt.wantErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + assert.Equal(t, tt.status, w.Code) + return + } + + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.status, w.Code) + }) + } + }) +} From 66931155058550b5a2cb157173757952bdec321e Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Tue, 14 Jan 2025 13:05:43 +0100 Subject: [PATCH 04/16] Resolved the review comments --- README.md | 47 ------ documentation/docs/guides/testing.md | 156 ++++++++++++++++++++ mock_context.go | 176 +++++++++++++++++++++-- mock_context_test.go | 204 +++++++++++++++++++-------- 4 files changed, 469 insertions(+), 114 deletions(-) create mode 100644 documentation/docs/guides/testing.md diff --git a/README.md b/README.md index cea13b1e..6d1984a5 100644 --- a/README.md +++ b/README.md @@ -363,50 +363,3 @@ Go instead of Node, I'm happy to use it. [gin-gonic-issue]: https://github.com/gin-gonic/gin/issues/155 [contributors-url]: https://github.com/go-fuego/fuego/graphs/contributors - -## Testing with Mock Context - -Fuego provides a framework-agnostic mock context for testing your controllers. This allows you to test your business logic without depending on specific web frameworks. - -### Basic Usage - -```go -func TestMyController(t *testing.T) { - // Create a mock context with your request body type - ctx := fuego.NewMockContext[UserRequest]() - - // Set up test data - ctx.SetBody(UserRequest{ - Name: "John Doe", - Email: "john@example.com", - }) - - // Add query parameters if needed - ctx.SetURLValues(url.Values{ - "filter": []string{"active"}, - }) - - // Add path parameters - ctx.SetPathParam("id", "123") - - // Call your controller - result, err := MyController(ctx) - - // Assert results - assert.NoError(t, err) - assert.Equal(t, expectedResult, result) -} -``` - -### Features - -The mock context supports: - -- Type-safe request bodies with generics -- URL query parameters -- Path parameters -- Headers -- Custom context values -- Request/Response objects - -This makes it easy to test your controllers without worrying about HTTP mechanics or framework specifics. diff --git a/documentation/docs/guides/testing.md b/documentation/docs/guides/testing.md new file mode 100644 index 00000000..9ef1bf02 --- /dev/null +++ b/documentation/docs/guides/testing.md @@ -0,0 +1,156 @@ +# Testing Fuego Controllers + +Fuego provides a `MockContext` type that makes it easy to test your controllers without setting up a full HTTP server. This guide will show you how to use it effectively. + +## Using MockContext + +The `MockContext` type implements the `ContextWithBody` interface, allowing you to test your controllers in isolation. Here's a simple example: + +```go +func TestMyController(t *testing.T) { + // Create a new mock context with your request body type + ctx := fuego.NewMockContext[MyRequestType]() + + // Set the request body + ctx.SetBody(MyRequestType{ + Name: "John", + Age: 30, + }) + + // Call your controller + response, err := MyController(ctx) + + // Assert the results + assert.NoError(t, err) + assert.Equal(t, expectedResponse, response) +} +``` + +## Complete Example + +Here's a more complete example showing how to test a controller that uses request body, query parameters, and validation: + +```go +// UserSearchRequest represents the search criteria +type UserSearchRequest struct { + MinAge int `json:"minAge" validate:"gte=0,lte=150"` + MaxAge int `json:"maxAge" validate:"gte=0,lte=150"` + NameQuery string `json:"nameQuery" validate:"required"` +} + +// SearchUsersController is our controller to test +func SearchUsersController(c fuego.ContextWithBody[UserSearchRequest]) (UserSearchResponse, error) { + body, err := c.Body() + if err != nil { + return UserSearchResponse{}, err + } + + // Get pagination from query params + page := c.QueryParamInt("page") + if page < 1 { + page = 1 + } + + // Business logic validation + if body.MinAge > body.MaxAge { + return UserSearchResponse{}, errors.New("minAge cannot be greater than maxAge") + } + + // ... rest of the controller logic +} + +func TestSearchUsersController(t *testing.T) { + tests := []struct { + name string + body UserSearchRequest + queryParams map[string]string + expectedError string + expected UserSearchResponse + }{ + { + name: "successful search", + body: UserSearchRequest{ + MinAge: 20, + MaxAge: 35, + NameQuery: "John", + }, + queryParams: map[string]string{ + "page": "1", + }, + expected: UserSearchResponse{ + // ... expected response + }, + }, + { + name: "invalid age range", + body: UserSearchRequest{ + MinAge: 40, + MaxAge: 20, + NameQuery: "John", + }, + expectedError: "minAge cannot be greater than maxAge", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mock context and set up the test + ctx := fuego.NewMockContext[UserSearchRequest]() + ctx.SetBody(tt.body) + + // Set query parameters + if tt.queryParams != nil { + for key, value := range tt.queryParams { + ctx.SetURLValues(map[string][]string{ + key: {value}, + }) + } + } + + // Call the controller + response, err := SearchUsersController(ctx) + + // Check error cases + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + + // Check success cases + assert.NoError(t, err) + assert.Equal(t, tt.expected, response) + }) + } +} +``` + +## Available Mock Methods + +`MockContext` provides several methods to help you test different aspects of your controllers: + +- `SetBody(body B)` - Set the request body +- `SetURLValues(values url.Values)` - Set query parameters +- `SetHeader(key, value string)` - Set request headers +- `SetPathParam(name, value string)` - Set path parameters +- `SetCookie(cookie http.Cookie)` - Set request cookies +- `SetContext(ctx context.Context)` - Set a custom context +- `SetResponse(w http.ResponseWriter)` - Set a custom response writer +- `SetRequest(r *http.Request)` - Set a custom request + +## Best Practices + +1. **Test Edge Cases**: Test both valid and invalid inputs, including validation errors. +2. **Use Table-Driven Tests**: Structure your tests as a slice of test cases for better organization. +3. **Mock Only What You Need**: Only set up the mock data that your test actually requires. +4. **Test Business Logic**: Focus on testing your business logic rather than the framework itself. +5. **Keep Tests Focused**: Each test should verify one specific behavior. + +## Why Use MockContext? + +We use `MockContext` with setter methods (instead of exported fields) to: + +- Maintain encapsulation and consistency with real implementations +- Allow for future additions like validation or logging without breaking user code +- Ensure tests reflect how the code will behave in production + +This approach makes your tests more maintainable and reliable while keeping them simple to write. diff --git a/mock_context.go b/mock_context.go index 4b89fe65..c71429d9 100644 --- a/mock_context.go +++ b/mock_context.go @@ -4,6 +4,9 @@ import ( "context" "net/http" "net/url" + "strconv" + "strings" + "time" ) // MockContext provides a framework-agnostic implementation of ContextWithBody @@ -17,6 +20,8 @@ type MockContext[B any] struct { ctx context.Context response http.ResponseWriter request *http.Request + cookies map[string]*http.Cookie + params map[string]OpenAPIParam } // NewMockContext creates a new MockContext instance with initialized maps @@ -28,18 +33,50 @@ func NewMockContext[B any]() *MockContext[B] { headers: make(http.Header), pathParams: make(map[string]string), ctx: context.Background(), + cookies: make(map[string]*http.Cookie), + params: make(map[string]OpenAPIParam), } } -// Body returns the previously set body value. This method always returns -// nil as the error value, as the mock context doesn't perform actual -// deserialization. +// GetOpenAPIParams returns the OpenAPI parameters for validation +func (m *MockContext[B]) GetOpenAPIParams() map[string]OpenAPIParam { + return m.params +} + +// SetOpenAPIParam sets an OpenAPI parameter for validation +func (m *MockContext[B]) SetOpenAPIParam(name string, param OpenAPIParam) { + m.params[name] = param +} + +// HasQueryParam checks if a query parameter exists +func (m *MockContext[B]) HasQueryParam(key string) bool { + _, exists := m.urlValues[key] + return exists +} + +// HasHeader checks if a header exists +func (m *MockContext[B]) HasHeader(key string) bool { + _, exists := m.headers[key] + return exists +} + +// HasCookie checks if a cookie exists +func (m *MockContext[B]) HasCookie(key string) bool { + _, exists := m.cookies[key] + return exists +} + +// Body returns the previously set body value func (m *MockContext[B]) Body() (B, error) { return m.body, nil } -// SetBody stores the provided body value for later retrieval via Body(). -// This is typically used in tests to simulate request bodies. +// MustBody returns the body or panics if there's an error +func (m *MockContext[B]) MustBody() B { + return m.body +} + +// SetBody stores the provided body value for later retrieval func (m *MockContext[B]) SetBody(body B) { m.body = body } @@ -54,9 +91,9 @@ func (m *MockContext[B]) SetURLValues(values url.Values) { m.urlValues = values } -// Header returns the mock headers -func (m *MockContext[B]) Header() http.Header { - return m.headers +// Header returns the value of the specified header +func (m *MockContext[B]) Header(key string) string { + return m.headers.Get(key) } // SetHeader sets a mock header @@ -102,4 +139,125 @@ func (m *MockContext[B]) Request() *http.Request { // SetRequest sets the mock request func (m *MockContext[B]) SetRequest(r *http.Request) { m.request = r -} \ No newline at end of file +} + +// Deadline implements context.Context +func (m *MockContext[B]) Deadline() (deadline time.Time, ok bool) { + return m.ctx.Deadline() +} + +// Done implements context.Context +func (m *MockContext[B]) Done() <-chan struct{} { + return m.ctx.Done() +} + +// Err implements context.Context +func (m *MockContext[B]) Err() error { + return m.ctx.Err() +} + +// Value implements context.Context +func (m *MockContext[B]) Value(key any) any { + return m.ctx.Value(key) +} + +// Cookie returns a mock cookie +func (m *MockContext[B]) Cookie(name string) (*http.Cookie, error) { + cookie, exists := m.cookies[name] + if !exists { + return nil, http.ErrNoCookie + } + return cookie, nil +} + +// SetCookie sets a mock cookie +func (m *MockContext[B]) SetCookie(cookie http.Cookie) { + m.cookies[cookie.Name] = &cookie +} + +// QueryParam returns the value of the specified query parameter +func (m *MockContext[B]) QueryParam(name string) string { + return m.urlValues.Get(name) +} + +// QueryParamArr returns the values of the specified query parameter +func (m *MockContext[B]) QueryParamArr(name string) []string { + return m.urlValues[name] +} + +// QueryParamInt returns the value of the specified query parameter as an integer +func (m *MockContext[B]) QueryParamInt(name string) int { + val := m.QueryParam(name) + if val == "" { + return 0 + } + i, _ := strconv.Atoi(val) + return i +} + +// QueryParamIntErr returns the value of the specified query parameter as an integer and any error +func (m *MockContext[B]) QueryParamIntErr(name string) (int, error) { + val := m.QueryParam(name) + if val == "" { + return 0, nil + } + return strconv.Atoi(val) +} + +// QueryParamBool returns the value of the specified query parameter as a boolean +func (m *MockContext[B]) QueryParamBool(name string) bool { + val := m.QueryParam(name) + if val == "" { + return false + } + b, _ := strconv.ParseBool(val) + return b +} + +// QueryParamBoolErr returns the value of the specified query parameter as a boolean and any error +func (m *MockContext[B]) QueryParamBoolErr(name string) (bool, error) { + val := m.QueryParam(name) + if val == "" { + return false, nil + } + return strconv.ParseBool(val) +} + +// QueryParams returns all query parameters +func (m *MockContext[B]) QueryParams() url.Values { + return m.urlValues +} + +// MainLang returns the main language from Accept-Language header +func (m *MockContext[B]) MainLang() string { + lang := m.headers.Get("Accept-Language") + if lang == "" { + return "" + } + return strings.Split(strings.Split(lang, ",")[0], "-")[0] +} + +// MainLocale returns the main locale from Accept-Language header +func (m *MockContext[B]) MainLocale() string { + return m.headers.Get("Accept-Language") +} + +// SetStatus sets the response status code +func (m *MockContext[B]) SetStatus(code int) { + if m.response != nil { + m.response.WriteHeader(code) + } +} + +// Redirect returns a redirect response +func (m *MockContext[B]) Redirect(code int, url string) (any, error) { + if m.response != nil { + http.Redirect(m.response, m.request, url, code) + } + return nil, nil +} + +// Render is a mock implementation that does nothing +func (m *MockContext[B]) Render(templateToExecute string, data any, templateGlobsToOverride ...string) (CtxRenderer, error) { + return nil, nil +} diff --git a/mock_context_test.go b/mock_context_test.go index 8118ba3a..cc6c3a5e 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -1,74 +1,162 @@ -package fuego +package fuego_test import ( - "context" - "net/http" - "net/http/httptest" - "net/url" + "errors" "testing" + "github.com/go-fuego/fuego" "github.com/stretchr/testify/assert" ) -type TestBody struct { - Name string `json:"name"` - Age int `json:"age"` +// UserSearchRequest represents the search criteria for users +type UserSearchRequest struct { + MinAge int `json:"minAge" validate:"gte=0,lte=150"` + MaxAge int `json:"maxAge" validate:"gte=0,lte=150"` + NameQuery string `json:"nameQuery" validate:"required"` } -func TestMockContext(t *testing.T) { - // Create a new mock context - ctx := NewMockContext[TestBody]() +// UserSearchResponse represents the search results +type UserSearchResponse struct { + Users []UserProfile `json:"users"` + TotalCount int `json:"totalCount"` + CurrentPage int `json:"currentPage"` +} + +// UserProfile represents a user in the system +type UserProfile struct { + ID string `json:"id"` + Name string `json:"name" validate:"required"` + Age int `json:"age" validate:"gte=0,lte=150"` + Email string `json:"email" validate:"required,email"` +} + +// SearchUsersController is an example of a real controller that would be used in a Fuego app +func SearchUsersController(c fuego.ContextWithBody[UserSearchRequest]) (UserSearchResponse, error) { + // Get and validate the request body + body, err := c.Body() + if err != nil { + return UserSearchResponse{}, err + } - // Test body - body := TestBody{ - Name: "John", - Age: 30, + // Get pagination parameters from query + page := c.QueryParamInt("page") + if page < 1 { + page = 1 } - ctx.SetBody(body) - gotBody, err := ctx.Body() - assert.NoError(t, err) - assert.Equal(t, body, gotBody) - - // Test URL values - values := url.Values{ - "key": []string{"value"}, + perPage := c.QueryParamInt("perPage") + if perPage < 1 || perPage > 100 { + perPage = 20 + } + + // Example validation beyond struct tags + if body.MinAge > body.MaxAge { + return UserSearchResponse{}, errors.New("minAge cannot be greater than maxAge") + } + + // In a real app, this would query a database + // Here we just return mock data that matches the criteria + users := []UserProfile{ + {ID: "user_1", Name: "John Doe", Age: 25, Email: "john@example.com"}, + {ID: "user_2", Name: "Jane Smith", Age: 30, Email: "jane@example.com"}, } - ctx.SetURLValues(values) - assert.Equal(t, values, ctx.URLValues()) - // Test headers - ctx.SetHeader("Content-Type", "application/json") - assert.Equal(t, "application/json", ctx.Header().Get("Content-Type")) + // Filter users based on criteria (simplified example) + var filteredUsers []UserProfile + for _, user := range users { + if user.Age >= body.MinAge && user.Age <= body.MaxAge { + filteredUsers = append(filteredUsers, user) + } + } - // Test path params - ctx.SetPathParam("id", "123") - assert.Equal(t, "123", ctx.PathParam("id")) + return UserSearchResponse{ + Users: filteredUsers, + TotalCount: len(filteredUsers), + CurrentPage: page, + }, nil } -func TestMockContextAdvanced(t *testing.T) { - // Test with custom context - ctx := NewMockContext[TestBody]() - customCtx := context.WithValue(context.Background(), "key", "value") - ctx.SetContext(customCtx) - assert.Equal(t, "value", ctx.Context().Value("key")) - - // Test with request/response - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/test", nil) - ctx.SetResponse(w) - ctx.SetRequest(r) - assert.Equal(t, w, ctx.Response()) - assert.Equal(t, r, ctx.Request()) - - // Test multiple headers - ctx.SetHeader("X-Test-1", "value1") - ctx.SetHeader("X-Test-2", "value2") - assert.Equal(t, "value1", ctx.Header().Get("X-Test-1")) - assert.Equal(t, "value2", ctx.Header().Get("X-Test-2")) - - // Test multiple path params - ctx.SetPathParam("id", "123") - ctx.SetPathParam("category", "books") - assert.Equal(t, "123", ctx.PathParam("id")) - assert.Equal(t, "books", ctx.PathParam("category")) -} \ No newline at end of file +func TestSearchUsersController(t *testing.T) { + tests := []struct { + name string + body UserSearchRequest + queryParams map[string]string + expectedError string + expected UserSearchResponse + }{ + { + name: "successful search with age range", + body: UserSearchRequest{ + MinAge: 20, + MaxAge: 35, + NameQuery: "John", + }, + queryParams: map[string]string{ + "page": "1", + "perPage": "20", + }, + expected: UserSearchResponse{ + Users: []UserProfile{ + {ID: "user_1", Name: "John Doe", Age: 25, Email: "john@example.com"}, + {ID: "user_2", Name: "Jane Smith", Age: 30, Email: "jane@example.com"}, + }, + TotalCount: 2, + CurrentPage: 1, + }, + }, + { + name: "invalid age range", + body: UserSearchRequest{ + MinAge: 40, + MaxAge: 20, + NameQuery: "John", + }, + expectedError: "minAge cannot be greater than maxAge", + }, + { + name: "default pagination values", + body: UserSearchRequest{ + MinAge: 20, + MaxAge: 35, + NameQuery: "John", + }, + expected: UserSearchResponse{ + Users: []UserProfile{ + {ID: "user_1", Name: "John Doe", Age: 25, Email: "john@example.com"}, + {ID: "user_2", Name: "Jane Smith", Age: 30, Email: "jane@example.com"}, + }, + TotalCount: 2, + CurrentPage: 1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mock context and set up the test case + ctx := fuego.NewMockContext[UserSearchRequest]() + ctx.SetBody(tt.body) + + // Set query parameters + if tt.queryParams != nil { + for key, value := range tt.queryParams { + ctx.SetURLValues(map[string][]string{ + key: {value}, + }) + } + } + + // Call the controller + response, err := SearchUsersController(ctx) + + // Check error cases + if tt.expectedError != "" { + assert.EqualError(t, err, tt.expectedError) + return + } + + // Check success cases + assert.NoError(t, err) + assert.Equal(t, tt.expected, response) + }) + } +} From 50d594cee8a7e4443a7c2ddbbd3add131d80ad94 Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Tue, 14 Jan 2025 13:18:59 +0100 Subject: [PATCH 05/16] Removed the TESTING.md file --- TESTING.md | 195 ----------------------------------------------------- 1 file changed, 195 deletions(-) delete mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index 27bd6f4b..00000000 --- a/TESTING.md +++ /dev/null @@ -1,195 +0,0 @@ -# Testing in Fuego - -This guide explains how to effectively test your Fuego applications, with a focus on using the mock context for testing controllers. - -## Mock Context - -Fuego provides a framework-agnostic mock context that allows you to test your controllers without depending on specific web frameworks. This makes it easy to focus on testing your business logic rather than HTTP mechanics. - -### Basic Usage - -```go -func TestMyController(t *testing.T) { - // Create a mock context with your request body type - ctx := fuego.NewMockContext[UserRequest]() - - // Set up test data - ctx.SetBody(UserRequest{ - Name: "John Doe", - Email: "john@example.com", - }) - - // Add query parameters if needed - ctx.SetURLValues(url.Values{ - "filter": []string{"active"}, - }) - - // Add path parameters - ctx.SetPathParam("id", "123") - - // Call your controller - result, err := MyController(ctx) - - // Assert results - assert.NoError(t, err) - assert.Equal(t, expectedResult, result) -} -``` - -### Features - -The mock context supports: - -- Type-safe request bodies with generics -- URL query parameters -- Path parameters -- Headers -- Custom context values -- Request/Response objects - -### Advanced Usage - -#### Testing with Headers - -```go -func TestControllerWithHeaders(t *testing.T) { - ctx := fuego.NewMockContext[EmptyBody]() - ctx.SetHeader("Authorization", "Bearer token123") - ctx.SetHeader("Content-Type", "application/json") - - result, err := MyAuthenticatedController(ctx) - assert.NoError(t, err) -} -``` - -#### Testing with Custom Context Values - -```go -func TestControllerWithContext(t *testing.T) { - ctx := fuego.NewMockContext[EmptyBody]() - customCtx := context.WithValue(context.Background(), "user_id", "123") - ctx.SetContext(customCtx) - - result, err := MyContextAwareController(ctx) - assert.NoError(t, err) -} -``` - -#### Testing with Request/Response Objects - -```go -func TestControllerWithRequestResponse(t *testing.T) { - ctx := fuego.NewMockContext[EmptyBody]() - w := httptest.NewRecorder() - r := httptest.NewRequest("GET", "/test", nil) - - ctx.SetResponse(w) - ctx.SetRequest(r) - - result, err := MyController(ctx) - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, w.Code) -} -``` - -### Best Practices - -1. **Use Table-Driven Tests** - -```go -func TestUserController(t *testing.T) { - tests := []struct { - name string - body UserRequest - want UserResponse - wantErr bool - }{ - { - name: "valid user", - body: UserRequest{Name: "John", Email: "john@example.com"}, - want: UserResponse{ID: "123", Name: "John"}, - }, - { - name: "invalid email", - body: UserRequest{Name: "John", Email: "invalid"}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx := fuego.NewMockContext[UserRequest]() - ctx.SetBody(tt.body) - - got, err := CreateUser(ctx) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tt.want, got) - }) - } -} -``` - -2. **Test Error Cases** - -```go -func TestErrorHandling(t *testing.T) { - ctx := fuego.NewMockContext[UserRequest]() - ctx.SetBody(UserRequest{}) // Empty body should trigger validation error - - _, err := CreateUser(ctx) - assert.Error(t, err) - assert.Contains(t, err.Error(), "validation failed") -} -``` - -3. **Test Validation Rules** - -```go -func TestValidation(t *testing.T) { - ctx := fuego.NewMockContext[UserRequest]() - ctx.SetBody(UserRequest{ - Name: "", // Required field - Email: "invalid-email", // Invalid format - }) - - _, err := CreateUser(ctx) - assert.Error(t, err) -} -``` - -4. **Test Middleware Integration** - -```go -func TestWithMiddleware(t *testing.T) { - ctx := fuego.NewMockContext[EmptyBody]() - ctx.SetHeader("Authorization", "Bearer valid-token") - - // Test that middleware allows the request - result, err := AuthMiddleware(MyProtectedController)(ctx) - assert.NoError(t, err) - - // Test that middleware blocks unauthorized requests - ctx.SetHeader("Authorization", "invalid-token") - _, err = AuthMiddleware(MyProtectedController)(ctx) - assert.Error(t, err) -} -``` - -### Tips for Effective Testing - -1. Keep tests focused on business logic -2. Use meaningful test names that describe the scenario -3. Test both success and failure cases -4. Use helper functions for common test setup -5. Test validation rules thoroughly -6. Mock external dependencies when needed -7. Use subtests for better organization -8. Test edge cases and boundary conditions - -## Contributing - -If you find any issues or have suggestions for improving the testing utilities, please open an issue or submit a pull request. From 13dda83aa1ef2cff9ae64ef90e4f4ddcdfe09b6a Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Wed, 15 Jan 2025 11:01:06 +0100 Subject: [PATCH 06/16] Documentation and testing updates --- README.md | 4 -- documentation/docs/guides/testing.md | 64 ++++++++++++---------------- mock_context.go | 41 ++++++++++-------- mock_context_test.go | 22 ++++------ serve_test.go | 6 ++- 5 files changed, 64 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 07e6dc6b..6d1984a5 100644 --- a/README.md +++ b/README.md @@ -361,9 +361,5 @@ Go instead of Node, I'm happy to use it. [MIT](./LICENSE.txt) -## Testing - -For information about testing your Fuego applications, please see [TESTING.md](TESTING.md). - [gin-gonic-issue]: https://github.com/gin-gonic/gin/issues/155 [contributors-url]: https://github.com/go-fuego/fuego/graphs/contributors diff --git a/documentation/docs/guides/testing.md b/documentation/docs/guides/testing.md index 9ef1bf02..d35ea3bd 100644 --- a/documentation/docs/guides/testing.md +++ b/documentation/docs/guides/testing.md @@ -1,10 +1,10 @@ # Testing Fuego Controllers -Fuego provides a `MockContext` type that makes it easy to test your controllers without setting up a full HTTP server. This guide will show you how to use it effectively. +Fuego provides a `MockContext` type that makes it easy to test your controllers without using httptest, allowing you to focus on your business logic instead of the HTTP layer. ## Using MockContext -The `MockContext` type implements the `ContextWithBody` interface, allowing you to test your controllers in isolation. Here's a simple example: +The `MockContext` type implements the `ContextWithBody` interface. Here's a simple example: ```go func TestMyController(t *testing.T) { @@ -20,14 +20,24 @@ func TestMyController(t *testing.T) { // Call your controller response, err := MyController(ctx) - // Assert the results + // Assert the results, using the well-known testify library assert.NoError(t, err) assert.Equal(t, expectedResponse, response) + + // Or, using the standard library + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !reflect.DeepEqual(expectedResponse, response) { + t.Fatalf("unexpected response: %v", response) + } } ``` ## Complete Example +Please refer to the [mock_context_test.go](https://github.com/go-fuego/fuego/blob/main/mock_context_test.go) file in the fuego repository for a complete and updated example. + Here's a more complete example showing how to test a controller that uses request body, query parameters, and validation: ```go @@ -63,7 +73,7 @@ func TestSearchUsersController(t *testing.T) { tests := []struct { name string body UserSearchRequest - queryParams map[string]string + queryParams url.Values expectedError string expected UserSearchResponse }{ @@ -74,8 +84,8 @@ func TestSearchUsersController(t *testing.T) { MaxAge: 35, NameQuery: "John", }, - queryParams: map[string]string{ - "page": "1", + queryParams: map[string][]string{ + "page": {"1"}, }, expected: UserSearchResponse{ // ... expected response @@ -97,15 +107,7 @@ func TestSearchUsersController(t *testing.T) { // Create mock context and set up the test ctx := fuego.NewMockContext[UserSearchRequest]() ctx.SetBody(tt.body) - - // Set query parameters - if tt.queryParams != nil { - for key, value := range tt.queryParams { - ctx.SetURLValues(map[string][]string{ - key: {value}, - }) - } - } + ctx.SetQueryParams(tt.queryParams) // Call the controller response, err := SearchUsersController(ctx) @@ -126,31 +128,21 @@ func TestSearchUsersController(t *testing.T) { ## Available Mock Methods -`MockContext` provides several methods to help you test different aspects of your controllers: +Provide external HTTP elements in `MockContext` with the following setters: -- `SetBody(body B)` - Set the request body -- `SetURLValues(values url.Values)` - Set query parameters -- `SetHeader(key, value string)` - Set request headers -- `SetPathParam(name, value string)` - Set path parameters -- `SetCookie(cookie http.Cookie)` - Set request cookies -- `SetContext(ctx context.Context)` - Set a custom context -- `SetResponse(w http.ResponseWriter)` - Set a custom response writer -- `SetRequest(r *http.Request)` - Set a custom request +- `SetBody(body B)` +- `SetQueryParams(values url.Values)` +- `SetHeader(key, value string)` +- `SetPathParam(name, value string)` +- `SetCookie(cookie http.Cookie)` +- `SetContext(ctx context.Context)` +- `SetResponse(w http.ResponseWriter)` +- `SetRequest(r *http.Request)` ## Best Practices 1. **Test Edge Cases**: Test both valid and invalid inputs, including validation errors. 2. **Use Table-Driven Tests**: Structure your tests as a slice of test cases for better organization. -3. **Mock Only What You Need**: Only set up the mock data that your test actually requires. +3. **Mock using interfaces**: Use interfaces to mock dependencies and make your controllers testable, just as we're doing here: the controller accept an interface, we're passing a mock implementation of context in the tests. 4. **Test Business Logic**: Focus on testing your business logic rather than the framework itself. -5. **Keep Tests Focused**: Each test should verify one specific behavior. - -## Why Use MockContext? - -We use `MockContext` with setter methods (instead of exported fields) to: - -- Maintain encapsulation and consistency with real implementations -- Allow for future additions like validation or logging without breaking user code -- Ensure tests reflect how the code will behave in production - -This approach makes your tests more maintainable and reliable while keeping them simple to write. +5. **Fuzz Testing**: Use fuzz testing to automatically find edge cases that you might have missed. User input can be anything! diff --git a/mock_context.go b/mock_context.go index 914a54c7..818013cb 100644 --- a/mock_context.go +++ b/mock_context.go @@ -9,12 +9,26 @@ import ( "time" ) +// NewMockContext creates a new MockContext instance with initialized maps +// for URL values, headers, and path parameters. It uses context.Background() +// as the default context. +func NewMockContext[B any]() *MockContext[B] { + return &MockContext[B]{ + urlValues: make(url.Values), + headers: make(http.Header), + pathParams: make(map[string]string), + ctx: context.Background(), + cookies: make(map[string]*http.Cookie), + params: make(map[string]OpenAPIParam), + } +} + // MockContext provides a framework-agnostic implementation of ContextWithBody // for testing purposes. It allows testing controllers without depending on // specific web frameworks like Gin or Echo. type MockContext[B any] struct { body B - urlValues url.Values + urlValues url.Values // query parameters headers http.Header pathParams map[string]string ctx context.Context @@ -24,19 +38,7 @@ type MockContext[B any] struct { params map[string]OpenAPIParam } -// NewMockContext creates a new MockContext instance with initialized maps -// for URL values, headers, and path parameters. It uses context.Background() -// as the default context. -func NewMockContext[B any]() *MockContext[B] { - return &MockContext[B]{ - urlValues: make(url.Values), - headers: make(http.Header), - pathParams: make(map[string]string), - ctx: context.Background(), - cookies: make(map[string]*http.Cookie), - params: make(map[string]OpenAPIParam), - } -} +var _ ContextWithBody[string] = &MockContext[string]{} // GetOpenAPIParams returns the OpenAPI parameters for validation func (m *MockContext[B]) GetOpenAPIParams() map[string]OpenAPIParam { @@ -68,7 +70,7 @@ func (m *MockContext[B]) HasCookie(key string) bool { // Body returns the previously set body value func (m *MockContext[B]) Body() (B, error) { - return m.body, nil + return TransformAndValidate(m, m.body) } // MustBody returns the body or panics if there's an error @@ -86,11 +88,16 @@ func (m *MockContext[B]) URLValues() url.Values { return m.urlValues } -// SetURLValues sets the mock URL values -func (m *MockContext[B]) SetURLValues(values url.Values) { +// SetQueryParams sets the mock URL values +func (m *MockContext[B]) SetQueryParams(values url.Values) { m.urlValues = values } +// SetQueryParam sets a single mock URL value +func (m *MockContext[B]) SetQueryParam(key, value string) { + m.urlValues.Set(key, value) +} + // Header returns the value of the specified header func (m *MockContext[B]) Header(key string) string { return m.headers.Get(key) diff --git a/mock_context_test.go b/mock_context_test.go index cc6c3a5e..b0e7cd24 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -2,10 +2,12 @@ package fuego_test import ( "errors" + "net/url" "testing" - "github.com/go-fuego/fuego" "github.com/stretchr/testify/assert" + + "github.com/go-fuego/fuego" ) // UserSearchRequest represents the search criteria for users @@ -79,7 +81,7 @@ func TestSearchUsersController(t *testing.T) { tests := []struct { name string body UserSearchRequest - queryParams map[string]string + queryParams url.Values expectedError string expected UserSearchResponse }{ @@ -90,9 +92,9 @@ func TestSearchUsersController(t *testing.T) { MaxAge: 35, NameQuery: "John", }, - queryParams: map[string]string{ - "page": "1", - "perPage": "20", + queryParams: map[string][]string{ + "page": {"1"}, + "perPage": {"20"}, }, expected: UserSearchResponse{ Users: []UserProfile{ @@ -135,15 +137,7 @@ func TestSearchUsersController(t *testing.T) { // Create mock context and set up the test case ctx := fuego.NewMockContext[UserSearchRequest]() ctx.SetBody(tt.body) - - // Set query parameters - if tt.queryParams != nil { - for key, value := range tt.queryParams { - ctx.SetURLValues(map[string][]string{ - key: {value}, - }) - } - } + ctx.SetQueryParams(tt.queryParams) // Call the controller response, err := SearchUsersController(ctx) diff --git a/serve_test.go b/serve_test.go index 0021afc4..31a448c8 100644 --- a/serve_test.go +++ b/serve_test.go @@ -57,8 +57,10 @@ func (t testOutTransformerOnNotReceiver) OutTransform(ctx context.Context) error return nil } -var _ OutTransformer = &testOutTransformer{} -var _ OutTransformer = &testOutTransformerOnNotReceiver{} +var ( + _ OutTransformer = &testOutTransformer{} + _ OutTransformer = &testOutTransformerOnNotReceiver{} +) func testControllerWithOutTransformer(c ContextNoBody) (testOutTransformer, error) { return testOutTransformer{Name: "John"}, nil From f05144692286bc78718083483ad5681b06a1031a Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Wed, 15 Jan 2025 11:35:58 +0100 Subject: [PATCH 07/16] Use internal.CommonContext to provide most functions in MockContext --- mock_context.go | 118 ++++++------------------------------------------ 1 file changed, 15 insertions(+), 103 deletions(-) diff --git a/mock_context.go b/mock_context.go index 818013cb..fd8ce5f2 100644 --- a/mock_context.go +++ b/mock_context.go @@ -4,9 +4,9 @@ import ( "context" "net/http" "net/url" - "strconv" "strings" - "time" + + "github.com/go-fuego/fuego/internal" ) // NewMockContext creates a new MockContext instance with initialized maps @@ -14,12 +14,12 @@ import ( // as the default context. func NewMockContext[B any]() *MockContext[B] { return &MockContext[B]{ - urlValues: make(url.Values), + CommonContext: internal.CommonContext[B]{ + CommonCtx: context.Background(), + }, headers: make(http.Header), pathParams: make(map[string]string), - ctx: context.Background(), cookies: make(map[string]*http.Cookie), - params: make(map[string]OpenAPIParam), } } @@ -27,32 +27,27 @@ func NewMockContext[B any]() *MockContext[B] { // for testing purposes. It allows testing controllers without depending on // specific web frameworks like Gin or Echo. type MockContext[B any] struct { - body B - urlValues url.Values // query parameters + internal.CommonContext[B] + + body B + headers http.Header pathParams map[string]string - ctx context.Context response http.ResponseWriter request *http.Request cookies map[string]*http.Cookie - params map[string]OpenAPIParam } var _ ContextWithBody[string] = &MockContext[string]{} -// GetOpenAPIParams returns the OpenAPI parameters for validation -func (m *MockContext[B]) GetOpenAPIParams() map[string]OpenAPIParam { - return m.params -} - // SetOpenAPIParam sets an OpenAPI parameter for validation func (m *MockContext[B]) SetOpenAPIParam(name string, param OpenAPIParam) { - m.params[name] = param + m.CommonContext.OpenAPIParams[name] = param } // HasQueryParam checks if a query parameter exists func (m *MockContext[B]) HasQueryParam(key string) bool { - _, exists := m.urlValues[key] + _, exists := m.UrlValues[key] return exists } @@ -83,19 +78,14 @@ func (m *MockContext[B]) SetBody(body B) { m.body = body } -// URLValues returns the mock URL values -func (m *MockContext[B]) URLValues() url.Values { - return m.urlValues -} - // SetQueryParams sets the mock URL values func (m *MockContext[B]) SetQueryParams(values url.Values) { - m.urlValues = values + m.UrlValues = values } // SetQueryParam sets a single mock URL value func (m *MockContext[B]) SetQueryParam(key, value string) { - m.urlValues.Set(key, value) + m.UrlValues.Set(key, value) } // Header returns the value of the specified header @@ -123,14 +113,9 @@ func (m *MockContext[B]) SetPathParam(name, value string) { m.pathParams[name] = value } -// Context returns the mock context -func (m *MockContext[B]) Context() context.Context { - return m.ctx -} - // SetContext sets the mock context func (m *MockContext[B]) SetContext(ctx context.Context) { - m.ctx = ctx + m.CommonContext.CommonCtx = ctx } // Response returns the mock response writer @@ -153,26 +138,6 @@ func (m *MockContext[B]) SetRequest(r *http.Request) { m.request = r } -// Deadline implements context.Context -func (m *MockContext[B]) Deadline() (deadline time.Time, ok bool) { - return m.ctx.Deadline() -} - -// Done implements context.Context -func (m *MockContext[B]) Done() <-chan struct{} { - return m.ctx.Done() -} - -// Err implements context.Context -func (m *MockContext[B]) Err() error { - return m.ctx.Err() -} - -// Value implements context.Context -func (m *MockContext[B]) Value(key any) any { - return m.ctx.Value(key) -} - // Cookie returns a mock cookie func (m *MockContext[B]) Cookie(name string) (*http.Cookie, error) { cookie, exists := m.cookies[name] @@ -187,59 +152,6 @@ func (m *MockContext[B]) SetCookie(cookie http.Cookie) { m.cookies[cookie.Name] = &cookie } -// QueryParam returns the value of the specified query parameter -func (m *MockContext[B]) QueryParam(name string) string { - return m.urlValues.Get(name) -} - -// QueryParamArr returns the values of the specified query parameter -func (m *MockContext[B]) QueryParamArr(name string) []string { - return m.urlValues[name] -} - -// QueryParamInt returns the value of the specified query parameter as an integer -func (m *MockContext[B]) QueryParamInt(name string) int { - val := m.QueryParam(name) - if val == "" { - return 0 - } - i, _ := strconv.Atoi(val) - return i -} - -// QueryParamIntErr returns the value of the specified query parameter as an integer and any error -func (m *MockContext[B]) QueryParamIntErr(name string) (int, error) { - val := m.QueryParam(name) - if val == "" { - return 0, nil - } - return strconv.Atoi(val) -} - -// QueryParamBool returns the value of the specified query parameter as a boolean -func (m *MockContext[B]) QueryParamBool(name string) bool { - val := m.QueryParam(name) - if val == "" { - return false - } - b, _ := strconv.ParseBool(val) - return b -} - -// QueryParamBoolErr returns the value of the specified query parameter as a boolean and any error -func (m *MockContext[B]) QueryParamBoolErr(name string) (bool, error) { - val := m.QueryParam(name) - if val == "" { - return false, nil - } - return strconv.ParseBool(val) -} - -// QueryParams returns all query parameters -func (m *MockContext[B]) QueryParams() url.Values { - return m.urlValues -} - // MainLang returns the main language from Accept-Language header func (m *MockContext[B]) MainLang() string { lang := m.headers.Get("Accept-Language") @@ -271,5 +183,5 @@ func (m *MockContext[B]) Redirect(code int, url string) (any, error) { // Render is a mock implementation that does nothing func (m *MockContext[B]) Render(templateToExecute string, data any, templateGlobsToOverride ...string) (CtxRenderer, error) { - return nil, nil + panic("not implemented") } From e3237c310180cfdfd3e42f289edb6f810b9234d9 Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Thu, 16 Jan 2025 13:33:11 +0100 Subject: [PATCH 08/16] refactor(mock): make MockContext fields public and leverage CommonContext functionality --- documentation/docs/guides/testing.md | 108 ++++++++--- mock_context.go | 260 +++++++-------------------- mock_context_test.go | 28 ++- 3 files changed, 175 insertions(+), 221 deletions(-) diff --git a/documentation/docs/guides/testing.md b/documentation/docs/guides/testing.md index 9ef1bf02..dacc6078 100644 --- a/documentation/docs/guides/testing.md +++ b/documentation/docs/guides/testing.md @@ -12,10 +12,10 @@ func TestMyController(t *testing.T) { ctx := fuego.NewMockContext[MyRequestType]() // Set the request body - ctx.SetBody(MyRequestType{ + ctx.BodyData = MyRequestType{ Name: "John", Age: 30, - }) + } // Call your controller response, err := MyController(ctx) @@ -96,15 +96,27 @@ func TestSearchUsersController(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Create mock context and set up the test ctx := fuego.NewMockContext[UserSearchRequest]() - ctx.SetBody(tt.body) + ctx.BodyData = tt.body + + // Set up OpenAPI parameters for validation + ctx.OpenAPIParams = map[string]internal.OpenAPIParam{ + "page": { + Name: "page", + Description: "Page number", + Type: fuego.QueryParamType, + GoType: "integer", + Default: 1, + }, + // Add other OpenAPI parameters as needed + } // Set query parameters if tt.queryParams != nil { + values := make(url.Values) for key, value := range tt.queryParams { - ctx.SetURLValues(map[string][]string{ - key: {value}, - }) + values.Set(key, value) } + ctx.UrlValues = values } // Call the controller @@ -124,18 +136,72 @@ func TestSearchUsersController(t *testing.T) { } ``` -## Available Mock Methods +## Available Fields and Methods + +`MockContext` provides public fields for easy testing: + +- `BodyData` - The request body +- `HeadersData` - HTTP headers +- `PathParamsData` - Path parameters +- `ResponseData` - Response writer +- `RequestData` - HTTP request +- `CookiesData` - HTTP cookies +- `UrlValues` (from CommonContext) - Query parameters +- `OpenAPIParams` (from CommonContext) - OpenAPI parameter definitions + +And implements all the methods from `ContextWithBody`: + +- `Body()` - Get the request body +- `MustBody()` - Get the request body (panics on error) +- `Header(key string)` - Get request header +- `HasHeader(key string)` - Check if header exists +- `PathParam(name string)` - Get path parameter +- `Cookie(name string)` - Get request cookie +- `HasCookie(key string)` - Check if cookie exists +- `Response()` - Get response writer +- `Request()` - Get request + +Additionally, since `MockContext` embeds `CommonContext`, you get access to all the common functionality: + +- `QueryParam(name string)` - Get query parameter +- `QueryParamInt(name string)` - Get query parameter as int +- `QueryParamBool(name string)` - Get query parameter as bool +- `QueryParamArr(name string)` - Get query parameter as string array +- `HasQueryParam(name string)` - Check if query parameter exists + +## OpenAPI Parameter Setup + +When using query parameters, you should define their OpenAPI specifications to avoid warnings and ensure proper validation: + +```go +ctx.OpenAPIParams = map[string]internal.OpenAPIParam{ + "page": { + Name: "page", + Description: "Page number", + Type: fuego.QueryParamType, + GoType: "integer", + Default: 1, + }, + "perPage": { + Name: "perPage", + Description: "Items per page", + Type: fuego.QueryParamType, + GoType: "integer", + Default: 20, + }, +} +``` -`MockContext` provides several methods to help you test different aspects of your controllers: +Available OpenAPI parameter fields: -- `SetBody(body B)` - Set the request body -- `SetURLValues(values url.Values)` - Set query parameters -- `SetHeader(key, value string)` - Set request headers -- `SetPathParam(name, value string)` - Set path parameters -- `SetCookie(cookie http.Cookie)` - Set request cookies -- `SetContext(ctx context.Context)` - Set a custom context -- `SetResponse(w http.ResponseWriter)` - Set a custom response writer -- `SetRequest(r *http.Request)` - Set a custom request +- `Name` - Parameter name +- `Description` - Parameter description +- `Type` - Parameter type (QueryParamType, HeaderParamType, CookieParamType) +- `GoType` - Go type ("integer", "string", "boolean") +- `Default` - Default value +- `Required` - Whether the parameter is required +- `Nullable` - Whether the parameter can be null +- `Examples` - Example values for documentation ## Best Practices @@ -144,13 +210,15 @@ func TestSearchUsersController(t *testing.T) { 3. **Mock Only What You Need**: Only set up the mock data that your test actually requires. 4. **Test Business Logic**: Focus on testing your business logic rather than the framework itself. 5. **Keep Tests Focused**: Each test should verify one specific behavior. +6. **Define OpenAPI Parameters**: Always define OpenAPI parameters for query parameters to ensure proper validation. ## Why Use MockContext? -We use `MockContext` with setter methods (instead of exported fields) to: +The `MockContext` implementation: -- Maintain encapsulation and consistency with real implementations -- Allow for future additions like validation or logging without breaking user code -- Ensure tests reflect how the code will behave in production +- Embeds `CommonContext` to provide consistent behavior with real implementations +- Provides public fields for easy testing while maintaining interface compatibility +- Handles OpenAPI validation and parameter processing through CommonContext +- Makes tests easy to write and maintain This approach makes your tests more maintainable and reliable while keeping them simple to write. diff --git a/mock_context.go b/mock_context.go index 914a54c7..fccfdf1c 100644 --- a/mock_context.go +++ b/mock_context.go @@ -4,24 +4,22 @@ import ( "context" "net/http" "net/url" - "strconv" "strings" - "time" + + "github.com/go-fuego/fuego/internal" ) // MockContext provides a framework-agnostic implementation of ContextWithBody // for testing purposes. It allows testing controllers without depending on // specific web frameworks like Gin or Echo. type MockContext[B any] struct { - body B - urlValues url.Values - headers http.Header - pathParams map[string]string - ctx context.Context - response http.ResponseWriter - request *http.Request - cookies map[string]*http.Cookie - params map[string]OpenAPIParam + internal.CommonContext[B] + BodyData B // Public for easy testing + HeadersData http.Header + PathParamsData map[string]string + ResponseData http.ResponseWriter + RequestData *http.Request + CookiesData map[string]*http.Cookie } // NewMockContext creates a new MockContext instance with initialized maps @@ -29,240 +27,108 @@ type MockContext[B any] struct { // as the default context. func NewMockContext[B any]() *MockContext[B] { return &MockContext[B]{ - urlValues: make(url.Values), - headers: make(http.Header), - pathParams: make(map[string]string), - ctx: context.Background(), - cookies: make(map[string]*http.Cookie), - params: make(map[string]OpenAPIParam), + CommonContext: internal.CommonContext[B]{ + CommonCtx: context.Background(), + UrlValues: make(url.Values), + OpenAPIParams: make(map[string]internal.OpenAPIParam), + }, + HeadersData: make(http.Header), + PathParamsData: make(map[string]string), + CookiesData: make(map[string]*http.Cookie), } } -// GetOpenAPIParams returns the OpenAPI parameters for validation -func (m *MockContext[B]) GetOpenAPIParams() map[string]OpenAPIParam { - return m.params +// Body returns the body value - implements ContextWithBody +func (m *MockContext[B]) Body() (B, error) { + return m.BodyData, nil } -// SetOpenAPIParam sets an OpenAPI parameter for validation -func (m *MockContext[B]) SetOpenAPIParam(name string, param OpenAPIParam) { - m.params[name] = param +// MustBody returns the body or panics if there's an error - implements ContextWithBody +func (m *MockContext[B]) MustBody() B { + return m.BodyData } -// HasQueryParam checks if a query parameter exists -func (m *MockContext[B]) HasQueryParam(key string) bool { - _, exists := m.urlValues[key] - return exists +// Header returns the value of the specified header - implements ContextWithBody +func (m *MockContext[B]) Header(key string) string { + return m.HeadersData.Get(key) } -// HasHeader checks if a header exists +// HasHeader checks if a header exists - implements ContextWithBody func (m *MockContext[B]) HasHeader(key string) bool { - _, exists := m.headers[key] + _, exists := m.HeadersData[key] return exists } -// HasCookie checks if a cookie exists +// HasCookie checks if a cookie exists - implements ContextWithBody func (m *MockContext[B]) HasCookie(key string) bool { - _, exists := m.cookies[key] + _, exists := m.CookiesData[key] return exists } -// Body returns the previously set body value -func (m *MockContext[B]) Body() (B, error) { - return m.body, nil -} - -// MustBody returns the body or panics if there's an error -func (m *MockContext[B]) MustBody() B { - return m.body -} - -// SetBody stores the provided body value for later retrieval -func (m *MockContext[B]) SetBody(body B) { - m.body = body -} - -// URLValues returns the mock URL values -func (m *MockContext[B]) URLValues() url.Values { - return m.urlValues -} - -// SetURLValues sets the mock URL values -func (m *MockContext[B]) SetURLValues(values url.Values) { - m.urlValues = values -} - -// Header returns the value of the specified header -func (m *MockContext[B]) Header(key string) string { - return m.headers.Get(key) -} - -// SetHeader sets a mock header -func (m *MockContext[B]) SetHeader(key, value string) { - m.headers.Set(key, value) -} - -// GetHeaders returns all headers (helper method for testing) -func (m *MockContext[B]) GetHeaders() http.Header { - return m.headers -} - -// PathParam returns a mock path parameter +// PathParam returns a mock path parameter - implements ContextWithBody func (m *MockContext[B]) PathParam(name string) string { - return m.pathParams[name] + return m.PathParamsData[name] } -// SetPathParam sets a mock path parameter -func (m *MockContext[B]) SetPathParam(name, value string) { - m.pathParams[name] = value -} - -// Context returns the mock context -func (m *MockContext[B]) Context() context.Context { - return m.ctx -} - -// SetContext sets the mock context -func (m *MockContext[B]) SetContext(ctx context.Context) { - m.ctx = ctx -} - -// Response returns the mock response writer +// Response returns the mock response writer - implements ContextWithBody func (m *MockContext[B]) Response() http.ResponseWriter { - return m.response -} - -// SetResponse sets the mock response writer -func (m *MockContext[B]) SetResponse(w http.ResponseWriter) { - m.response = w + return m.ResponseData } -// Request returns the mock request +// Request returns the mock request - implements ContextWithBody func (m *MockContext[B]) Request() *http.Request { - return m.request -} - -// SetRequest sets the mock request -func (m *MockContext[B]) SetRequest(r *http.Request) { - m.request = r -} - -// Deadline implements context.Context -func (m *MockContext[B]) Deadline() (deadline time.Time, ok bool) { - return m.ctx.Deadline() -} - -// Done implements context.Context -func (m *MockContext[B]) Done() <-chan struct{} { - return m.ctx.Done() + return m.RequestData } -// Err implements context.Context -func (m *MockContext[B]) Err() error { - return m.ctx.Err() -} - -// Value implements context.Context -func (m *MockContext[B]) Value(key any) any { - return m.ctx.Value(key) -} - -// Cookie returns a mock cookie +// Cookie returns a mock cookie - implements ContextWithBody func (m *MockContext[B]) Cookie(name string) (*http.Cookie, error) { - cookie, exists := m.cookies[name] + cookie, exists := m.CookiesData[name] if !exists { return nil, http.ErrNoCookie } return cookie, nil } -// SetCookie sets a mock cookie -func (m *MockContext[B]) SetCookie(cookie http.Cookie) { - m.cookies[cookie.Name] = &cookie -} - -// QueryParam returns the value of the specified query parameter -func (m *MockContext[B]) QueryParam(name string) string { - return m.urlValues.Get(name) -} - -// QueryParamArr returns the values of the specified query parameter -func (m *MockContext[B]) QueryParamArr(name string) []string { - return m.urlValues[name] -} - -// QueryParamInt returns the value of the specified query parameter as an integer -func (m *MockContext[B]) QueryParamInt(name string) int { - val := m.QueryParam(name) - if val == "" { - return 0 - } - i, _ := strconv.Atoi(val) - return i -} - -// QueryParamIntErr returns the value of the specified query parameter as an integer and any error -func (m *MockContext[B]) QueryParamIntErr(name string) (int, error) { - val := m.QueryParam(name) - if val == "" { - return 0, nil - } - return strconv.Atoi(val) -} - -// QueryParamBool returns the value of the specified query parameter as a boolean -func (m *MockContext[B]) QueryParamBool(name string) bool { - val := m.QueryParam(name) - if val == "" { - return false - } - b, _ := strconv.ParseBool(val) - return b -} - -// QueryParamBoolErr returns the value of the specified query parameter as a boolean and any error -func (m *MockContext[B]) QueryParamBoolErr(name string) (bool, error) { - val := m.QueryParam(name) - if val == "" { - return false, nil - } - return strconv.ParseBool(val) -} - -// QueryParams returns all query parameters -func (m *MockContext[B]) QueryParams() url.Values { - return m.urlValues -} - -// MainLang returns the main language from Accept-Language header +// MainLang returns the main language from Accept-Language header - implements ContextWithBody func (m *MockContext[B]) MainLang() string { - lang := m.headers.Get("Accept-Language") + lang := m.HeadersData.Get("Accept-Language") if lang == "" { return "" } return strings.Split(strings.Split(lang, ",")[0], "-")[0] } -// MainLocale returns the main locale from Accept-Language header +// MainLocale returns the main locale from Accept-Language header - implements ContextWithBody func (m *MockContext[B]) MainLocale() string { - return m.headers.Get("Accept-Language") + return m.HeadersData.Get("Accept-Language") } -// SetStatus sets the response status code -func (m *MockContext[B]) SetStatus(code int) { - if m.response != nil { - m.response.WriteHeader(code) - } -} - -// Redirect returns a redirect response +// Redirect returns a redirect response - implements ContextWithBody func (m *MockContext[B]) Redirect(code int, url string) (any, error) { - if m.response != nil { - http.Redirect(m.response, m.request, url, code) + if m.ResponseData != nil { + http.Redirect(m.ResponseData, m.RequestData, url, code) } return nil, nil } -// Render is a mock implementation that does nothing +// Render is a mock implementation that does nothing - implements ContextWithBody func (m *MockContext[B]) Render(templateToExecute string, data any, templateGlobsToOverride ...string) (CtxRenderer, error) { return nil, nil } + +// SetStatus sets the response status code - implements ContextWithBody +func (m *MockContext[B]) SetStatus(code int) { + if m.ResponseData != nil { + m.ResponseData.WriteHeader(code) + } +} + +// SetCookie sets a mock cookie - implements ContextWithBody +func (m *MockContext[B]) SetCookie(cookie http.Cookie) { + m.CookiesData[cookie.Name] = &cookie +} + +// SetHeader sets a mock header - implements ContextWithBody +func (m *MockContext[B]) SetHeader(key, value string) { + m.HeadersData.Set(key, value) +} diff --git a/mock_context_test.go b/mock_context_test.go index cc6c3a5e..0f2a0410 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -2,9 +2,11 @@ package fuego_test import ( "errors" + "net/url" "testing" "github.com/go-fuego/fuego" + "github.com/go-fuego/fuego/internal" "github.com/stretchr/testify/assert" ) @@ -134,15 +136,33 @@ func TestSearchUsersController(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Create mock context and set up the test case ctx := fuego.NewMockContext[UserSearchRequest]() - ctx.SetBody(tt.body) + ctx.BodyData = tt.body + + // Set up OpenAPI parameters for pagination + ctx.OpenAPIParams = map[string]internal.OpenAPIParam{ + "page": { + Name: "page", + Description: "Page number", + Type: fuego.QueryParamType, + GoType: "integer", + Default: 1, + }, + "perPage": { + Name: "perPage", + Description: "Items per page", + Type: fuego.QueryParamType, + GoType: "integer", + Default: 20, + }, + } // Set query parameters if tt.queryParams != nil { + values := make(url.Values) for key, value := range tt.queryParams { - ctx.SetURLValues(map[string][]string{ - key: {value}, - }) + values.Set(key, value) } + ctx.UrlValues = values } // Call the controller From 488feb955b36194e69f4fe3aefdaaee285814ba4 Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Thu, 16 Jan 2025 18:40:29 +0100 Subject: [PATCH 09/16] feat(testing): Expose MockContext fields for simpler test setup Convert private fields to public and update docs with examples --- documentation/docs/guides/testing.md | 51 ++++------ mock_context.go | 141 +++++++++------------------ mock_context_test.go | 11 ++- 3 files changed, 68 insertions(+), 135 deletions(-) diff --git a/documentation/docs/guides/testing.md b/documentation/docs/guides/testing.md index d35ea3bd..9db02268 100644 --- a/documentation/docs/guides/testing.md +++ b/documentation/docs/guides/testing.md @@ -8,11 +8,8 @@ The `MockContext` type implements the `ContextWithBody` interface. Here's a simp ```go func TestMyController(t *testing.T) { - // Create a new mock context with your request body type - ctx := fuego.NewMockContext[MyRequestType]() - - // Set the request body - ctx.SetBody(MyRequestType{ + // Create a new mock context with the request body + ctx := fuego.NewMockContext(MyRequestType{ Name: "John", Age: 30, }) @@ -20,24 +17,14 @@ func TestMyController(t *testing.T) { // Call your controller response, err := MyController(ctx) - // Assert the results, using the well-known testify library + // Assert the results assert.NoError(t, err) assert.Equal(t, expectedResponse, response) - - // Or, using the standard library - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if !reflect.DeepEqual(expectedResponse, response) { - t.Fatalf("unexpected response: %v", response) - } } ``` ## Complete Example -Please refer to the [mock_context_test.go](https://github.com/go-fuego/fuego/blob/main/mock_context_test.go) file in the fuego repository for a complete and updated example. - Here's a more complete example showing how to test a controller that uses request body, query parameters, and validation: ```go @@ -84,7 +71,7 @@ func TestSearchUsersController(t *testing.T) { MaxAge: 35, NameQuery: "John", }, - queryParams: map[string][]string{ + queryParams: url.Values{ "page": {"1"}, }, expected: UserSearchResponse{ @@ -104,10 +91,11 @@ func TestSearchUsersController(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create mock context and set up the test - ctx := fuego.NewMockContext[UserSearchRequest]() - ctx.SetBody(tt.body) - ctx.SetQueryParams(tt.queryParams) + // Create mock context with the test body + ctx := fuego.NewMockContext(tt.body) + + // Set query parameters directly + ctx.UrlValues = tt.queryParams // Call the controller response, err := SearchUsersController(ctx) @@ -126,23 +114,20 @@ func TestSearchUsersController(t *testing.T) { } ``` -## Available Mock Methods +## Available Fields -Provide external HTTP elements in `MockContext` with the following setters: +The `MockContext` type provides the following public fields for testing: -- `SetBody(body B)` -- `SetQueryParams(values url.Values)` -- `SetHeader(key, value string)` -- `SetPathParam(name, value string)` -- `SetCookie(cookie http.Cookie)` -- `SetContext(ctx context.Context)` -- `SetResponse(w http.ResponseWriter)` -- `SetRequest(r *http.Request)` +- `RequestBody` - The request body of type B +- `Headers` - HTTP headers +- `PathParams` - URL path parameters +- `Cookies` - HTTP cookies +- `UrlValues` - Query parameters ## Best Practices 1. **Test Edge Cases**: Test both valid and invalid inputs, including validation errors. 2. **Use Table-Driven Tests**: Structure your tests as a slice of test cases for better organization. -3. **Mock using interfaces**: Use interfaces to mock dependencies and make your controllers testable, just as we're doing here: the controller accept an interface, we're passing a mock implementation of context in the tests. +3. **Mock using interfaces**: Use interfaces to mock dependencies and make your controllers testable. 4. **Test Business Logic**: Focus on testing your business logic rather than the framework itself. -5. **Fuzz Testing**: Use fuzz testing to automatically find edge cases that you might have missed. User input can be anything! +5. **Fuzz Testing**: Use fuzz testing to automatically find edge cases that you might have missed. diff --git a/mock_context.go b/mock_context.go index fd8ce5f2..792bda9d 100644 --- a/mock_context.go +++ b/mock_context.go @@ -3,119 +3,81 @@ package fuego import ( "context" "net/http" - "net/url" "strings" "github.com/go-fuego/fuego/internal" ) -// NewMockContext creates a new MockContext instance with initialized maps -// for URL values, headers, and path parameters. It uses context.Background() -// as the default context. -func NewMockContext[B any]() *MockContext[B] { - return &MockContext[B]{ - CommonContext: internal.CommonContext[B]{ - CommonCtx: context.Background(), - }, - headers: make(http.Header), - pathParams: make(map[string]string), - cookies: make(map[string]*http.Cookie), - } -} - // MockContext provides a framework-agnostic implementation of ContextWithBody // for testing purposes. It allows testing controllers without depending on // specific web frameworks like Gin or Echo. type MockContext[B any] struct { internal.CommonContext[B] - body B + // Public fields for easier testing + RequestBody B + Headers http.Header + PathParams map[string]string + response http.ResponseWriter + request *http.Request + Cookies map[string]*http.Cookie +} - headers http.Header - pathParams map[string]string - response http.ResponseWriter - request *http.Request - cookies map[string]*http.Cookie +// NewMockContext creates a new MockContext instance with the provided body +func NewMockContext[B any](body B) *MockContext[B] { + return &MockContext[B]{ + CommonContext: internal.CommonContext[B]{ + CommonCtx: context.Background(), + }, + RequestBody: body, + Headers: make(http.Header), + PathParams: make(map[string]string), + Cookies: make(map[string]*http.Cookie), + } } var _ ContextWithBody[string] = &MockContext[string]{} -// SetOpenAPIParam sets an OpenAPI parameter for validation -func (m *MockContext[B]) SetOpenAPIParam(name string, param OpenAPIParam) { - m.CommonContext.OpenAPIParams[name] = param +// Body returns the previously set body value +func (m *MockContext[B]) Body() (B, error) { + return m.RequestBody, nil } -// HasQueryParam checks if a query parameter exists -func (m *MockContext[B]) HasQueryParam(key string) bool { - _, exists := m.UrlValues[key] - return exists +// MustBody returns the body or panics if there's an error +func (m *MockContext[B]) MustBody() B { + return m.RequestBody } // HasHeader checks if a header exists func (m *MockContext[B]) HasHeader(key string) bool { - _, exists := m.headers[key] + _, exists := m.Headers[key] return exists } // HasCookie checks if a cookie exists func (m *MockContext[B]) HasCookie(key string) bool { - _, exists := m.cookies[key] + _, exists := m.Cookies[key] return exists } -// Body returns the previously set body value -func (m *MockContext[B]) Body() (B, error) { - return TransformAndValidate(m, m.body) -} - -// MustBody returns the body or panics if there's an error -func (m *MockContext[B]) MustBody() B { - return m.body -} - -// SetBody stores the provided body value for later retrieval -func (m *MockContext[B]) SetBody(body B) { - m.body = body -} - -// SetQueryParams sets the mock URL values -func (m *MockContext[B]) SetQueryParams(values url.Values) { - m.UrlValues = values -} - -// SetQueryParam sets a single mock URL value -func (m *MockContext[B]) SetQueryParam(key, value string) { - m.UrlValues.Set(key, value) -} - // Header returns the value of the specified header func (m *MockContext[B]) Header(key string) string { - return m.headers.Get(key) + return m.Headers.Get(key) } -// SetHeader sets a mock header +// SetHeader sets a header in the mock context func (m *MockContext[B]) SetHeader(key, value string) { - m.headers.Set(key, value) -} - -// GetHeaders returns all headers (helper method for testing) -func (m *MockContext[B]) GetHeaders() http.Header { - return m.headers + m.Headers.Set(key, value) } // PathParam returns a mock path parameter func (m *MockContext[B]) PathParam(name string) string { - return m.pathParams[name] -} - -// SetPathParam sets a mock path parameter -func (m *MockContext[B]) SetPathParam(name, value string) { - m.pathParams[name] = value + return m.PathParams[name] } -// SetContext sets the mock context -func (m *MockContext[B]) SetContext(ctx context.Context) { - m.CommonContext.CommonCtx = ctx +// Request returns the mock request +func (m *MockContext[B]) Request() *http.Request { + return m.request } // Response returns the mock response writer @@ -123,38 +85,30 @@ func (m *MockContext[B]) Response() http.ResponseWriter { return m.response } -// SetResponse sets the mock response writer -func (m *MockContext[B]) SetResponse(w http.ResponseWriter) { - m.response = w -} - -// Request returns the mock request -func (m *MockContext[B]) Request() *http.Request { - return m.request -} - -// SetRequest sets the mock request -func (m *MockContext[B]) SetRequest(r *http.Request) { - m.request = r +// SetStatus sets the response status code +func (m *MockContext[B]) SetStatus(code int) { + if m.response != nil { + m.response.WriteHeader(code) + } } // Cookie returns a mock cookie func (m *MockContext[B]) Cookie(name string) (*http.Cookie, error) { - cookie, exists := m.cookies[name] + cookie, exists := m.Cookies[name] if !exists { return nil, http.ErrNoCookie } return cookie, nil } -// SetCookie sets a mock cookie +// SetCookie sets a cookie in the mock context func (m *MockContext[B]) SetCookie(cookie http.Cookie) { - m.cookies[cookie.Name] = &cookie + m.Cookies[cookie.Name] = &cookie } // MainLang returns the main language from Accept-Language header func (m *MockContext[B]) MainLang() string { - lang := m.headers.Get("Accept-Language") + lang := m.Headers.Get("Accept-Language") if lang == "" { return "" } @@ -163,14 +117,7 @@ func (m *MockContext[B]) MainLang() string { // MainLocale returns the main locale from Accept-Language header func (m *MockContext[B]) MainLocale() string { - return m.headers.Get("Accept-Language") -} - -// SetStatus sets the response status code -func (m *MockContext[B]) SetStatus(code int) { - if m.response != nil { - m.response.WriteHeader(code) - } + return m.Headers.Get("Accept-Language") } // Redirect returns a redirect response diff --git a/mock_context_test.go b/mock_context_test.go index b0e7cd24..5f70eaeb 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -92,7 +92,7 @@ func TestSearchUsersController(t *testing.T) { MaxAge: 35, NameQuery: "John", }, - queryParams: map[string][]string{ + queryParams: url.Values{ "page": {"1"}, "perPage": {"20"}, }, @@ -134,10 +134,11 @@ func TestSearchUsersController(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create mock context and set up the test case - ctx := fuego.NewMockContext[UserSearchRequest]() - ctx.SetBody(tt.body) - ctx.SetQueryParams(tt.queryParams) + // Create mock context with the test body + ctx := fuego.NewMockContext(tt.body) + + // Set query parameters directly + ctx.UrlValues = tt.queryParams // Call the controller response, err := SearchUsersController(ctx) From d8c2637bf441796922d27a3b68deef0c853e56d5 Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Thu, 16 Jan 2025 19:43:23 +0100 Subject: [PATCH 10/16] fix(mock): Initialize OpenAPIParams in MockContext constructor Properly initialize CommonContext fields to prevent potential panics --- documentation/docs/guides/testing.md | 4 ++-- mock_context.go | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/documentation/docs/guides/testing.md b/documentation/docs/guides/testing.md index 9db02268..b67f46fa 100644 --- a/documentation/docs/guides/testing.md +++ b/documentation/docs/guides/testing.md @@ -9,7 +9,7 @@ The `MockContext` type implements the `ContextWithBody` interface. Here's a simp ```go func TestMyController(t *testing.T) { // Create a new mock context with the request body - ctx := fuego.NewMockContext(MyRequestType{ + ctx := fuego.NewMockContext[MyRequestType](MyRequestType{ Name: "John", Age: 30, }) @@ -92,7 +92,7 @@ func TestSearchUsersController(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Create mock context with the test body - ctx := fuego.NewMockContext(tt.body) + ctx := fuego.NewMockContext[UserSearchRequest](tt.body) // Set query parameters directly ctx.UrlValues = tt.queryParams diff --git a/mock_context.go b/mock_context.go index 792bda9d..de15dcd7 100644 --- a/mock_context.go +++ b/mock_context.go @@ -3,6 +3,7 @@ package fuego import ( "context" "net/http" + "net/url" "strings" "github.com/go-fuego/fuego/internal" @@ -27,7 +28,10 @@ type MockContext[B any] struct { func NewMockContext[B any](body B) *MockContext[B] { return &MockContext[B]{ CommonContext: internal.CommonContext[B]{ - CommonCtx: context.Background(), + CommonCtx: context.Background(), + UrlValues: make(url.Values), + OpenAPIParams: make(map[string]internal.OpenAPIParam), + DefaultStatusCode: http.StatusOK, }, RequestBody: body, Headers: make(http.Header), From bc99b13b59e1108a42fe165fc1e4340c6dbab18e Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Thu, 16 Jan 2025 19:50:43 +0100 Subject: [PATCH 11/16] Removed unused import --- mock_context_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/mock_context_test.go b/mock_context_test.go index 8f6cb7f9..5f70eaeb 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -5,7 +5,6 @@ import ( "net/url" "testing" - "github.com/go-fuego/fuego/internal" "github.com/stretchr/testify/assert" "github.com/go-fuego/fuego" From 50e87a739932dc4ce70ee6d60ed3add5873bb1d9 Mon Sep 17 00:00:00 2001 From: Olisa Agbafor Date: Fri, 17 Jan 2025 19:45:46 +0100 Subject: [PATCH 12/16] feat(mock): Add helper methods for setting query params with OpenAPI validation --- documentation/docs/guides/testing.md | 54 ++++++++++++++++++---------- mock_context.go | 46 ++++++++++++++++++++++++ mock_context_test.go | 15 ++++---- 3 files changed, 90 insertions(+), 25 deletions(-) diff --git a/documentation/docs/guides/testing.md b/documentation/docs/guides/testing.md index 1efd3a3e..222bed1d 100644 --- a/documentation/docs/guides/testing.md +++ b/documentation/docs/guides/testing.md @@ -12,7 +12,12 @@ func TestMyController(t *testing.T) { ctx := fuego.NewMockContext[MyRequestType](MyRequestType{ Name: "John", Age: 30, - } + }) + + // Add query parameters with OpenAPI validation + ctx.WithQueryParamInt("page", 1, + fuego.ParamDescription("Page number"), + fuego.ParamDefault(1)) // Call your controller response, err := MyController(ctx) @@ -60,7 +65,7 @@ func TestSearchUsersController(t *testing.T) { tests := []struct { name string body UserSearchRequest - queryParams url.Values + setupContext func(*fuego.MockContext[UserSearchRequest]) expectedError string expected UserSearchResponse }{ @@ -71,22 +76,19 @@ func TestSearchUsersController(t *testing.T) { MaxAge: 35, NameQuery: "John", }, - queryParams: url.Values{ - "page": {"1"}, + setupContext: func(ctx *fuego.MockContext[UserSearchRequest]) { + // Add query parameters with OpenAPI validation + ctx.WithQueryParamInt("page", 1, + fuego.ParamDescription("Page number"), + fuego.ParamDefault(1)) + ctx.WithQueryParamInt("perPage", 20, + fuego.ParamDescription("Items per page"), + fuego.ParamDefault(20)) }, expected: UserSearchResponse{ // ... expected response }, }, - { - name: "invalid age range", - body: UserSearchRequest{ - MinAge: 40, - MaxAge: 20, - NameQuery: "John", - }, - expectedError: "minAge cannot be greater than maxAge", - }, } for _, tt := range tests { @@ -94,8 +96,10 @@ func TestSearchUsersController(t *testing.T) { // Create mock context with the test body ctx := fuego.NewMockContext[UserSearchRequest](tt.body) - // Set query parameters directly - ctx.UrlValues = tt.queryParams + // Set up context with query parameters + if tt.setupContext != nil { + tt.setupContext(ctx) + } // Call the controller response, err := SearchUsersController(ctx) @@ -114,15 +118,29 @@ func TestSearchUsersController(t *testing.T) { } ``` -## Available Fields +## Available Fields and Methods + +The `MockContext` type provides the following: -The `MockContext` type provides the following public fields for testing: +Public Fields: - `RequestBody` - The request body of type B - `Headers` - HTTP headers - `PathParams` - URL path parameters - `Cookies` - HTTP cookies -- `UrlValues` - Query parameters + +Helper Methods for Query Parameters: + +- `WithQueryParam(name, value string, options ...func(*OpenAPIParam))` - Add a string query parameter +- `WithQueryParamInt(name string, value int, options ...func(*OpenAPIParam))` - Add an integer query parameter +- `WithQueryParamBool(name string, value bool, options ...func(*OpenAPIParam))` - Add a boolean query parameter + +Each helper method accepts OpenAPI parameter options like: + +- `ParamDescription(description string)` - Add parameter description +- `ParamDefault(value any)` - Set default value +- `ParamRequired()` - Mark parameter as required +- `ParamExample(name string, value any)` - Add example value ## Best Practices diff --git a/mock_context.go b/mock_context.go index de15dcd7..bf2ce127 100644 --- a/mock_context.go +++ b/mock_context.go @@ -2,6 +2,7 @@ package fuego import ( "context" + "fmt" "net/http" "net/url" "strings" @@ -136,3 +137,48 @@ func (m *MockContext[B]) Redirect(code int, url string) (any, error) { func (m *MockContext[B]) Render(templateToExecute string, data any, templateGlobsToOverride ...string) (CtxRenderer, error) { panic("not implemented") } + +// WithQueryParam adds a query parameter to the mock context with OpenAPI validation +func (m *MockContext[B]) WithQueryParam(name string, value string, options ...func(*OpenAPIParam)) *MockContext[B] { + param := OpenAPIParam{ + Name: name, + GoType: "string", + Type: "query", + } + for _, option := range options { + option(¶m) + } + m.CommonContext.OpenAPIParams[name] = param + m.CommonContext.UrlValues.Set(name, value) + return m +} + +// WithQueryParamInt adds an integer query parameter to the mock context with OpenAPI validation +func (m *MockContext[B]) WithQueryParamInt(name string, value int, options ...func(*OpenAPIParam)) *MockContext[B] { + param := OpenAPIParam{ + Name: name, + GoType: "integer", + Type: "query", + } + for _, option := range options { + option(¶m) + } + m.CommonContext.OpenAPIParams[name] = param + m.CommonContext.UrlValues.Set(name, fmt.Sprintf("%d", value)) + return m +} + +// WithQueryParamBool adds a boolean query parameter to the mock context with OpenAPI validation +func (m *MockContext[B]) WithQueryParamBool(name string, value bool, options ...func(*OpenAPIParam)) *MockContext[B] { + param := OpenAPIParam{ + Name: name, + GoType: "boolean", + Type: "query", + } + for _, option := range options { + option(¶m) + } + m.CommonContext.OpenAPIParams[name] = param + m.CommonContext.UrlValues.Set(name, fmt.Sprintf("%t", value)) + return m +} diff --git a/mock_context_test.go b/mock_context_test.go index 5f70eaeb..4cfc7e50 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -2,7 +2,6 @@ package fuego_test import ( "errors" - "net/url" "testing" "github.com/stretchr/testify/assert" @@ -81,7 +80,7 @@ func TestSearchUsersController(t *testing.T) { tests := []struct { name string body UserSearchRequest - queryParams url.Values + setupContext func(*fuego.MockContext[UserSearchRequest]) expectedError string expected UserSearchResponse }{ @@ -92,9 +91,9 @@ func TestSearchUsersController(t *testing.T) { MaxAge: 35, NameQuery: "John", }, - queryParams: url.Values{ - "page": {"1"}, - "perPage": {"20"}, + setupContext: func(ctx *fuego.MockContext[UserSearchRequest]) { + ctx.WithQueryParamInt("page", 1, fuego.ParamDescription("Page number"), fuego.ParamDefault(1)) + ctx.WithQueryParamInt("perPage", 20, fuego.ParamDescription("Items per page"), fuego.ParamDefault(20)) }, expected: UserSearchResponse{ Users: []UserProfile{ @@ -137,8 +136,10 @@ func TestSearchUsersController(t *testing.T) { // Create mock context with the test body ctx := fuego.NewMockContext(tt.body) - // Set query parameters directly - ctx.UrlValues = tt.queryParams + // Set up context with query parameters if provided + if tt.setupContext != nil { + tt.setupContext(ctx) + } // Call the controller response, err := SearchUsersController(ctx) From 8357ed8e6cb9a095b239b301d74f271b90c97171 Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Fri, 31 Jan 2025 15:14:44 +0100 Subject: [PATCH 13/16] Removed unused comment --- mock_context.go | 1 - 1 file changed, 1 deletion(-) diff --git a/mock_context.go b/mock_context.go index bf2ce127..9da6e106 100644 --- a/mock_context.go +++ b/mock_context.go @@ -16,7 +16,6 @@ import ( type MockContext[B any] struct { internal.CommonContext[B] - // Public fields for easier testing RequestBody B Headers http.Header PathParams map[string]string From be830f5bd292319b9d22d157b1cb8d7bbbf3415a Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Fri, 31 Jan 2025 16:00:49 +0100 Subject: [PATCH 14/16] Linting --- mock_context.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mock_context.go b/mock_context.go index 9da6e106..a364fa83 100644 --- a/mock_context.go +++ b/mock_context.go @@ -138,7 +138,7 @@ func (m *MockContext[B]) Render(templateToExecute string, data any, templateGlob } // WithQueryParam adds a query parameter to the mock context with OpenAPI validation -func (m *MockContext[B]) WithQueryParam(name string, value string, options ...func(*OpenAPIParam)) *MockContext[B] { +func (m *MockContext[B]) WithQueryParam(name, value string, options ...func(*OpenAPIParam)) *MockContext[B] { param := OpenAPIParam{ Name: name, GoType: "string", From f2ad6ccdad299e3d1d0db2ee582c9e4beb2f728a Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Fri, 31 Jan 2025 17:22:48 +0100 Subject: [PATCH 15/16] Adds `NewMockContextNoBody` to create a new MockContext suitable for a request & controller with no body Changed WithXxx to SetXxx for query parameters. Removed the ...option pattern for declaring query parameters in the MockContext. --- documentation/docs/guides/testing.md | 8 ++---- mock_context.go | 29 +++++++++---------- mock_context_test.go | 43 ++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/documentation/docs/guides/testing.md b/documentation/docs/guides/testing.md index 222bed1d..67f91220 100644 --- a/documentation/docs/guides/testing.md +++ b/documentation/docs/guides/testing.md @@ -9,15 +9,13 @@ The `MockContext` type implements the `ContextWithBody` interface. Here's a simp ```go func TestMyController(t *testing.T) { // Create a new mock context with the request body - ctx := fuego.NewMockContext[MyRequestType](MyRequestType{ + ctx := fuego.NewMockContext(MyRequestType{ Name: "John", Age: 30, }) - // Add query parameters with OpenAPI validation - ctx.WithQueryParamInt("page", 1, - fuego.ParamDescription("Page number"), - fuego.ParamDefault(1)) + // Add query parameters + ctx.SetQueryParamInt("page", 1) // Call your controller response, err := MyController(ctx) diff --git a/mock_context.go b/mock_context.go index a364fa83..2cf5cabf 100644 --- a/mock_context.go +++ b/mock_context.go @@ -40,6 +40,11 @@ func NewMockContext[B any](body B) *MockContext[B] { } } +// NewMockContextNoBody creates a new MockContext suitable for a request & controller with no body +func NewMockContextNoBody() *MockContext[any] { + return NewMockContext[any](nil) +} + var _ ContextWithBody[string] = &MockContext[string]{} // Body returns the previously set body value @@ -137,46 +142,40 @@ func (m *MockContext[B]) Render(templateToExecute string, data any, templateGlob panic("not implemented") } -// WithQueryParam adds a query parameter to the mock context with OpenAPI validation -func (m *MockContext[B]) WithQueryParam(name, value string, options ...func(*OpenAPIParam)) *MockContext[B] { +// SetQueryParam adds a query parameter to the mock context with OpenAPI validation +func (m *MockContext[B]) SetQueryParam(name, value string) *MockContext[B] { param := OpenAPIParam{ Name: name, GoType: "string", Type: "query", } - for _, option := range options { - option(¶m) - } + m.CommonContext.OpenAPIParams[name] = param m.CommonContext.UrlValues.Set(name, value) return m } -// WithQueryParamInt adds an integer query parameter to the mock context with OpenAPI validation -func (m *MockContext[B]) WithQueryParamInt(name string, value int, options ...func(*OpenAPIParam)) *MockContext[B] { +// SetQueryParamInt adds an integer query parameter to the mock context with OpenAPI validation +func (m *MockContext[B]) SetQueryParamInt(name string, value int) *MockContext[B] { param := OpenAPIParam{ Name: name, GoType: "integer", Type: "query", } - for _, option := range options { - option(¶m) - } + m.CommonContext.OpenAPIParams[name] = param m.CommonContext.UrlValues.Set(name, fmt.Sprintf("%d", value)) return m } -// WithQueryParamBool adds a boolean query parameter to the mock context with OpenAPI validation -func (m *MockContext[B]) WithQueryParamBool(name string, value bool, options ...func(*OpenAPIParam)) *MockContext[B] { +// SetQueryParamBool adds a boolean query parameter to the mock context with OpenAPI validation +func (m *MockContext[B]) SetQueryParamBool(name string, value bool) *MockContext[B] { param := OpenAPIParam{ Name: name, GoType: "boolean", Type: "query", } - for _, option := range options { - option(¶m) - } + m.CommonContext.OpenAPIParams[name] = param m.CommonContext.UrlValues.Set(name, fmt.Sprintf("%t", value)) return m diff --git a/mock_context_test.go b/mock_context_test.go index 4cfc7e50..be8925cd 100644 --- a/mock_context_test.go +++ b/mock_context_test.go @@ -5,8 +5,11 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/go-fuego/fuego" + "github.com/go-fuego/fuego/option" + "github.com/go-fuego/fuego/param" ) // UserSearchRequest represents the search criteria for users @@ -92,8 +95,8 @@ func TestSearchUsersController(t *testing.T) { NameQuery: "John", }, setupContext: func(ctx *fuego.MockContext[UserSearchRequest]) { - ctx.WithQueryParamInt("page", 1, fuego.ParamDescription("Page number"), fuego.ParamDefault(1)) - ctx.WithQueryParamInt("perPage", 20, fuego.ParamDescription("Items per page"), fuego.ParamDefault(20)) + ctx.SetQueryParamInt("page", 1) + ctx.SetQueryParamInt("perPage", 20) }, expected: UserSearchResponse{ Users: []UserProfile{ @@ -156,3 +159,39 @@ func TestSearchUsersController(t *testing.T) { }) } } + +func TestMockContextNoBody(t *testing.T) { + myController := func(c fuego.ContextNoBody) (string, error) { + return "Hello, " + c.QueryParam("name"), nil + } + + // Just check that `myController` is indeed an acceptable Fuego controller + s := fuego.NewServer() + fuego.Get(s, "/route", myController, + option.Query("name", "Name given to be greeted", param.Default("World")), + ) + + t.Run("TestMockContextNoBody", func(t *testing.T) { + ctx := fuego.NewMockContextNoBody() + assert.NotNil(t, ctx) + + ctx.SetQueryParam("name", "You") + + // Call the controller + response, err := myController(ctx) + + require.NoError(t, err) + require.Equal(t, "Hello, You", response) + }) + + t.Run("Does not use the default params from the route declaration", func(t *testing.T) { + ctx := fuego.NewMockContextNoBody() + assert.NotNil(t, ctx) + + // Call the controller + response, err := myController(ctx) + + require.NoError(t, err) + require.Equal(t, "Hello, ", response) + }) +} From 97f21efc1600934bdea102e1dc4b95c3c0e8fa76 Mon Sep 17 00:00:00 2001 From: EwenQuim Date: Sat, 1 Feb 2025 09:50:00 +0100 Subject: [PATCH 16/16] Removed the available fields and methods section from the testing guide It's not necessary to have the available fields and methods section in the testing guide, as it's already documented in the codebase & the godoc reference. --- documentation/docs/guides/testing.md | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/documentation/docs/guides/testing.md b/documentation/docs/guides/testing.md index 67f91220..d47b1080 100644 --- a/documentation/docs/guides/testing.md +++ b/documentation/docs/guides/testing.md @@ -116,30 +116,6 @@ func TestSearchUsersController(t *testing.T) { } ``` -## Available Fields and Methods - -The `MockContext` type provides the following: - -Public Fields: - -- `RequestBody` - The request body of type B -- `Headers` - HTTP headers -- `PathParams` - URL path parameters -- `Cookies` - HTTP cookies - -Helper Methods for Query Parameters: - -- `WithQueryParam(name, value string, options ...func(*OpenAPIParam))` - Add a string query parameter -- `WithQueryParamInt(name string, value int, options ...func(*OpenAPIParam))` - Add an integer query parameter -- `WithQueryParamBool(name string, value bool, options ...func(*OpenAPIParam))` - Add a boolean query parameter - -Each helper method accepts OpenAPI parameter options like: - -- `ParamDescription(description string)` - Add parameter description -- `ParamDefault(value any)` - Set default value -- `ParamRequired()` - Mark parameter as required -- `ParamExample(name string, value any)` - Add example value - ## Best Practices 1. **Test Edge Cases**: Test both valid and invalid inputs, including validation errors.