Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add framework-agnostic mock context for testing #351

Merged
merged 25 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dc235ba
feat: add framework-agnostic mock context for testing
olisaagbafor Jan 13, 2025
82afb5a
feat(test): add framework-agnostic mock context
olisaagbafor Jan 13, 2025
94d151c
feat(test): add realistic mock context example
olisaagbafor Jan 13, 2025
6693115
Resolved the review comments
olisaagbafor Jan 14, 2025
6ab9051
Merge branch 'workflow' into feature/mock-context-for-testing
olisaagbafor Jan 14, 2025
50d594c
Removed the TESTING.md file
olisaagbafor Jan 14, 2025
13dda83
Documentation and testing updates
EwenQuim Jan 15, 2025
3cea539
Merge branch 'main' into pr/olisaagbafor/351
EwenQuim Jan 15, 2025
f051446
Use internal.CommonContext to provide most functions in MockContext
EwenQuim Jan 15, 2025
e3237c3
refactor(mock): make MockContext fields public and leverage CommonCon…
olisaagbafor Jan 16, 2025
110137e
Merge branch 'main' into feature/mock-context-for-testing
olisaagbafor Jan 16, 2025
4792732
Merge branch 'dev-' into feature/mock-context-for-testing
olisaagbafor Jan 16, 2025
20732df
Merge branch 'feature/mock-context-for-testing' of github.com:olisaag…
olisaagbafor Jan 16, 2025
c2f039b
Merge branch 'feature/mock-context-for-testing' of github.com:olisaag…
olisaagbafor Jan 16, 2025
488feb9
feat(testing): Expose MockContext fields for simpler test setup
olisaagbafor Jan 16, 2025
08e3251
Merge branch 'Olisa' into feature/mock-context-for-testing
olisaagbafor Jan 16, 2025
d8c2637
fix(mock): Initialize OpenAPIParams in MockContext constructor
olisaagbafor Jan 16, 2025
5383101
Merge branch 'feature/mock-context-for-testing' of github.com:olisaag…
olisaagbafor Jan 16, 2025
bc99b13
Removed unused import
olisaagbafor Jan 16, 2025
50e87a7
feat(mock): Add helper methods for setting query params with OpenAPI …
olisaagbafor Jan 17, 2025
ccfa704
Merge branch 'main' into pr/olisaagbafor/351
EwenQuim Jan 29, 2025
8357ed8
Removed unused comment
EwenQuim Jan 31, 2025
be830f5
Linting
EwenQuim Jan 31, 2025
f2ad6cc
Adds `NewMockContextNoBody` to create a new MockContext suitable for …
EwenQuim Jan 31, 2025
97f21ef
Removed the available fields and methods section from the testing guide
EwenQuim Feb 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,3 +363,50 @@

[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
olisaagbafor marked this conversation as resolved.
Show resolved Hide resolved

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

Check failure on line 401 in README.md

View workflow job for this annotation

GitHub Actions / lint

Multiple headings with the same content [Context: "### 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.
105 changes: 105 additions & 0 deletions mock_context.go
Original file line number Diff line number Diff line change
@@ -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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not using a setter and not an exported attribute name?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use a setter method instead of an exported field to maintain encapsulation and consistency with real implementations. This allows to add validation or logging in the future without breaking user code and ensures tests reflect how the code will behave in production.


// 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
}

Check failure on line 105 in mock_context.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gci)
74 changes: 74 additions & 0 deletions mock_context_test.go
olisaagbafor marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package fuego
olisaagbafor marked this conversation as resolved.
Show resolved Hide resolved

import (
"context"
"net/http"

Check failure on line 5 in mock_context_test.go

View workflow job for this annotation

GitHub Actions / tests

"net/http" imported and not used
"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"))
}
Loading