264 lines
7.8 KiB
Markdown
264 lines
7.8 KiB
Markdown
# JSON-RPC Server Mock with DSL
|
|
|
|
## When to Use This Example
|
|
|
|
Use this when:
|
|
- Testing JSON-RPC clients
|
|
- Need to mock JSON-RPC server responses
|
|
- Want configurable mock behavior per method
|
|
- Need to track and assert on received requests
|
|
- Testing with OpenTelemetry trace propagation
|
|
|
|
**Dependency Level**: Level 1 (In-Memory) - Uses `httptest.Server` for in-memory HTTP
|
|
|
|
**Key Insight**: When testing a **JSON-RPC client**, mock the **server** it calls. Use rich DSL for readable test setup.
|
|
|
|
## Implementation
|
|
|
|
### Rich JSON-RPC Server Mock
|
|
|
|
```go
|
|
// internal/testutils/jrpc_server_mock.go
|
|
package testutils
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
|
|
"github.com/gorilla/rpc/v2/json2"
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/propagation"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
var ErrMethodNotFound = errors.New("method not found")
|
|
|
|
// TraceQuery holds received JSON-RPC queries for assertions
|
|
type TraceQuery struct {
|
|
Method string
|
|
Params string
|
|
}
|
|
|
|
// JrpcTraceServerMock is a rich JSON-RPC server mock with DSL.
|
|
// Uses httptest.Server for in-memory HTTP (Level 1).
|
|
type JrpcTraceServerMock struct {
|
|
tracer trace.Tracer
|
|
server *httptest.Server
|
|
mockResponses map[string]any // method -> response
|
|
queriesReceived []TraceQuery // for assertions
|
|
}
|
|
|
|
// StartJrpcTraceServerMock starts an in-memory JSON-RPC server.
|
|
// Returns a rich DSL object for configuring mock responses.
|
|
func StartJrpcTraceServerMock() *JrpcTraceServerMock {
|
|
mock := &JrpcTraceServerMock{
|
|
mockResponses: make(map[string]any),
|
|
tracer: otel.Tracer("trace-server-mock"),
|
|
}
|
|
|
|
mux := mock.createHTTPHandlers()
|
|
mock.server = httptest.NewServer(mux)
|
|
|
|
return mock
|
|
}
|
|
|
|
// AddMockResponse configures the mock to return a response for a method.
|
|
// This is the DSL - chain multiple calls for different methods!
|
|
func (m *JrpcTraceServerMock) AddMockResponse(method string, response any) {
|
|
m.mockResponses[method] = response
|
|
}
|
|
|
|
// GetQueriesReceived returns all queries received (for assertions)
|
|
func (m *JrpcTraceServerMock) GetQueriesReceived() []TraceQuery {
|
|
return m.queriesReceived
|
|
}
|
|
|
|
// Close shuts down the server (idempotent)
|
|
func (m *JrpcTraceServerMock) Close() {
|
|
m.server.Close()
|
|
}
|
|
|
|
// Address returns the server address (for client configuration)
|
|
func (m *JrpcTraceServerMock) Address() string {
|
|
return m.server.Listener.Addr().String()
|
|
}
|
|
|
|
func (m *JrpcTraceServerMock) createHTTPHandlers() *http.ServeMux {
|
|
mux := http.NewServeMux()
|
|
codec := json2.NewCodec()
|
|
|
|
mux.HandleFunc("/reader", func(w http.ResponseWriter, r *http.Request) {
|
|
// Extract OpenTelemetry context for realistic testing
|
|
reqCtx := r.Context()
|
|
reqCtx = otel.GetTextMapPropagator().Extract(reqCtx, propagation.HeaderCarrier(r.Header))
|
|
reqCtx, span := m.tracer.Start(reqCtx, "jrpc-trace-server",
|
|
trace.WithSpanKind(trace.SpanKindServer))
|
|
defer span.End()
|
|
|
|
if r.Method != http.MethodPost {
|
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
receivedReq := codec.NewRequest(r)
|
|
method, err := receivedReq.Method()
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Check if we have a mock response configured
|
|
if response, exists := m.mockResponses[method]; exists {
|
|
args := struct{}{}
|
|
if err := receivedReq.ReadRequest(&args); err != nil {
|
|
receivedReq.WriteError(w, http.StatusBadRequest, err)
|
|
return
|
|
}
|
|
|
|
// Store query for assertions
|
|
m.queriesReceived = append(m.queriesReceived, TraceQuery{
|
|
Method: method,
|
|
Params: fmt.Sprintf("%+v", args),
|
|
})
|
|
|
|
// Write mock response
|
|
receivedReq.WriteResponse(w, response)
|
|
return
|
|
}
|
|
|
|
// Method not configured
|
|
params := []string{}
|
|
receivedReq.ReadRequest(¶ms)
|
|
receivedReq.WriteError(w, http.StatusBadRequest, ErrMethodNotFound)
|
|
})
|
|
|
|
return mux
|
|
}
|
|
```
|
|
|
|
## Usage Examples
|
|
|
|
### Setup in Test Suite
|
|
|
|
```go
|
|
func (suite *TaskmonTestSuite) SetupSuite() {
|
|
// Start in-memory JSON-RPC server mock (Level 1)
|
|
suite.jrpcServerMock = testutils.StartJrpcTraceServerMock()
|
|
|
|
// Configure mock responses using DSL
|
|
suite.jrpcServerMock.AddMockResponse("protocol", struct {
|
|
Version string `json:"version"`
|
|
Date string `json:"date"`
|
|
}{
|
|
Version: "3.18.0",
|
|
Date: "Sep-04-2018",
|
|
})
|
|
|
|
suite.jrpcServerMock.AddMockResponse("get_traces", struct {
|
|
Traces []string `json:"traces"`
|
|
}{
|
|
Traces: []string{"trace1", "trace2"},
|
|
})
|
|
|
|
// Configure your client to use the mock server
|
|
client := jrpc.NewClient(suite.jrpcServerMock.Address() + "/reader")
|
|
}
|
|
|
|
func (suite *TaskmonTestSuite) TearDownSuite() {
|
|
suite.jrpcServerMock.Close()
|
|
}
|
|
```
|
|
|
|
### Test with Assertions
|
|
|
|
```go
|
|
func (suite *TaskmonTestSuite) TestProtocolVersion() {
|
|
// Call your code that makes JSON-RPC requests
|
|
version, err := suite.taskmon.GetProtocolVersion()
|
|
suite.Require().NoError(err)
|
|
suite.Equal("3.18.0", version.Version)
|
|
|
|
// Assert on received queries
|
|
queries := suite.jrpcServerMock.GetQueriesReceived()
|
|
suite.Require().Len(queries, 1)
|
|
suite.Equal("protocol", queries[0].Method)
|
|
}
|
|
|
|
func (suite *TaskmonTestSuite) TestGetTraces() {
|
|
// Call your code
|
|
traces, err := suite.taskmon.GetTraces()
|
|
suite.Require().NoError(err)
|
|
suite.Equal([]string{"trace1", "trace2"}, traces)
|
|
|
|
// Verify the right method was called
|
|
queries := suite.jrpcServerMock.GetQueriesReceived()
|
|
suite.Require().Len(queries, 2) // protocol + get_traces
|
|
suite.Equal("get_traces", queries[1].Method)
|
|
}
|
|
```
|
|
|
|
## Why This Pattern is Excellent
|
|
|
|
1. **Rich DSL** - `AddMockResponse()` for easy, readable configuration
|
|
2. **Readable Setup** - Tests are self-documenting, clear intent
|
|
3. **In-Memory** - Uses `httptest.Server` (Level 1, no network I/O)
|
|
4. **Query Tracking** - `GetQueriesReceived()` for assertions on what was called
|
|
5. **OpenTelemetry Integration** - Realistic trace propagation for observability testing
|
|
6. **Idempotent Cleanup** - Safe to call `Close()` multiple times
|
|
7. **Flexible** - Configure any method/response combination dynamically
|
|
|
|
## Key Design Principles
|
|
|
|
### DSL for Configuration
|
|
|
|
Mock setup should read like configuration:
|
|
```go
|
|
mock.AddMockResponse("method_name", expectedResponse)
|
|
mock.AddMockResponse("another_method", anotherResponse)
|
|
```
|
|
|
|
### Query Tracking for Assertions
|
|
|
|
Always track what was received:
|
|
- Method names called
|
|
- Parameters passed
|
|
- Order of calls
|
|
- Number of calls
|
|
|
|
### Built on httptest.Server
|
|
|
|
httptest.Server provides:
|
|
- In-memory HTTP (no network I/O)
|
|
- Automatic address allocation
|
|
- Clean lifecycle management
|
|
- Standard library, no dependencies
|
|
|
|
## Pattern Comparison
|
|
|
|
| Pattern | Use When |
|
|
|---------|----------|
|
|
| **httptest.Server** | Simple HTTP mocking |
|
|
| **NATS test harness** | Need real NATS (pub/sub) |
|
|
| **gRPC client mock** | Testing gRPC **server** |
|
|
| **JSON-RPC server mock** | Testing JSON-RPC **client** |
|
|
|
|
## Benefits
|
|
|
|
- **In-Memory** - No network I/O, pure Go
|
|
- **Fast** - Microsecond startup time
|
|
- **Configurable** - Dynamic response configuration per test
|
|
- **Trackable** - Full visibility into received requests
|
|
- **OpenTelemetry-aware** - Realistic trace propagation
|
|
- **Reusable** - Same infrastructure across test levels
|
|
|
|
## Key Takeaways
|
|
|
|
1. **Mock servers should have rich DSL** - Makes setup readable
|
|
2. **Track received requests** - Essential for assertions
|
|
3. **Use httptest.Server** - Perfect for HTTP-based protocols
|
|
4. **Make setup read like configuration** - Self-documenting tests
|
|
5. **Support trace propagation** - Realistic observability testing
|
|
6. **Idempotent cleanup** - Safe resource management
|