micro--go-micro
1bb25d6e7f
* feat: add agent platform showcase and blog post Add a complete platform example (Users, Posts, Comments, Mail) that mirrors micro/blog, demonstrating how existing microservices become AI-accessible through MCP with zero code changes. Includes blog post "Your Microservices Are Already an AI Platform" walking through real agent workflows: signup, content creation, commenting, tagging, and cross-service messaging. https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc * refactor: rename handler types to drop redundant Service suffix UserService → Users, PostService → Posts, CommentService → Comments, MailService → Mail. Matches micro/blog naming convention. https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc * refactor: consolidate top-level directories, reduce framework bloat Move internal/non-public packages behind internal/ or into their parent packages where they belong: - deploy/ → gateway/mcp/deploy/ (Helm charts belong with the gateway) - profile/ → service/profile/ (preset plugin profiles are a service concern) - scripts/ → internal/scripts/ (install script is not public API) - test/ → internal/test/ (test harness is not public API) - util/ → internal/util/ (internal helpers shouldn't be imported externally) Also fixes CLAUDE.md merge conflict markers and updates project structure documentation. All import paths updated. Build and tests pass. https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc * refactor: redesign model package to match framework conventions Rename model.Database interface to model.Model (consistent with client.Client, server.Server, store.Store). Remove generics in favor of interface{}-based API with reflection. Key changes: - model.Model interface: Register once, CRUD infers table from type - DefaultModel + NewModel() + package-level convenience functions - Schema registered via Register(&User{}), no per-call schema passing - Memory implementation as default (in model package, like store) - memory/sqlite/postgres backends updated for new interface - protoc-gen-micro generates RegisterXModel() instead of generic factory - All docs, blog, and README updated https://claude.ai/code/session_01GkduEhcrqcG45rdfYh8dAc --------- Co-authored-by: Claude <noreply@anthropic.com>
232 行
5.2 KiB
Go
232 行
5.2 KiB
Go
// Package test provides utilities for testing micro services.
|
|
//
|
|
// Due to go-micro's global defaults, running multiple services in one process
|
|
// requires careful isolation. This package provides helpers for the common case
|
|
// of testing a single service.
|
|
//
|
|
// Basic usage:
|
|
//
|
|
// func TestUserService(t *testing.T) {
|
|
// h := test.NewHarness(t)
|
|
// defer h.Stop()
|
|
//
|
|
// // Register your service handler
|
|
// h.Register(new(UsersHandler))
|
|
//
|
|
// // Start the harness
|
|
// h.Start()
|
|
//
|
|
// // Call the service
|
|
// var rsp UserResponse
|
|
// err := h.Call("Users.Create", &CreateRequest{Name: "Alice"}, &rsp)
|
|
// if err != nil {
|
|
// t.Fatal(err)
|
|
// }
|
|
// }
|
|
package test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"go-micro.dev/v5/broker"
|
|
"go-micro.dev/v5/client"
|
|
"go-micro.dev/v5/registry"
|
|
"go-micro.dev/v5/server"
|
|
"go-micro.dev/v5/transport"
|
|
)
|
|
|
|
// Harness provides an in-process test environment for a micro service
|
|
type Harness struct {
|
|
t *testing.T
|
|
name string
|
|
handler interface{}
|
|
registry registry.Registry
|
|
transport transport.Transport
|
|
broker broker.Broker
|
|
server server.Server
|
|
client client.Client
|
|
started bool
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewHarness creates a new test harness
|
|
func NewHarness(t *testing.T) *Harness {
|
|
// Create isolated instances for testing
|
|
reg := registry.NewMemoryRegistry()
|
|
tr := transport.NewHTTPTransport()
|
|
br := broker.NewMemoryBroker()
|
|
|
|
return &Harness{
|
|
t: t,
|
|
name: "test",
|
|
registry: reg,
|
|
transport: tr,
|
|
broker: br,
|
|
}
|
|
}
|
|
|
|
// Name sets the service name (default: "test")
|
|
func (h *Harness) Name(name string) *Harness {
|
|
h.name = name
|
|
return h
|
|
}
|
|
|
|
// Register sets the handler for the service
|
|
func (h *Harness) Register(handler interface{}) *Harness {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.started {
|
|
h.t.Fatal("cannot register handler after Start()")
|
|
}
|
|
|
|
h.handler = handler
|
|
return h
|
|
}
|
|
|
|
// Start starts the service
|
|
func (h *Harness) Start() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.started {
|
|
return
|
|
}
|
|
|
|
if h.handler == nil {
|
|
h.t.Fatal("no handler registered, call Register() first")
|
|
}
|
|
|
|
// Connect broker
|
|
if err := h.broker.Connect(); err != nil {
|
|
h.t.Fatalf("failed to connect broker: %v", err)
|
|
}
|
|
|
|
// Create server with isolated transport
|
|
h.server = server.NewServer(
|
|
server.Name(h.name),
|
|
server.Registry(h.registry),
|
|
server.Transport(h.transport),
|
|
server.Broker(h.broker),
|
|
server.Address("127.0.0.1:0"),
|
|
)
|
|
|
|
// Register handler
|
|
if err := h.server.Handle(h.server.NewHandler(h.handler)); err != nil {
|
|
h.t.Fatalf("failed to register handler: %v", err)
|
|
}
|
|
|
|
// Start server
|
|
if err := h.server.Start(); err != nil {
|
|
h.t.Fatalf("failed to start server: %v", err)
|
|
}
|
|
|
|
// Create client with same registry/transport
|
|
h.client = client.NewClient(
|
|
client.Registry(h.registry),
|
|
client.Transport(h.transport),
|
|
client.Broker(h.broker),
|
|
client.RequestTimeout(5*time.Second),
|
|
)
|
|
|
|
// Wait for registration
|
|
h.waitForService()
|
|
|
|
h.started = true
|
|
}
|
|
|
|
func (h *Harness) waitForService() {
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
services, err := h.registry.GetService(h.name)
|
|
if err == nil && len(services) > 0 && len(services[0].Nodes) > 0 {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
h.t.Fatalf("service %s did not register in time", h.name)
|
|
}
|
|
|
|
// Stop stops the service
|
|
func (h *Harness) Stop() {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if h.server != nil {
|
|
h.server.Stop()
|
|
}
|
|
if h.broker != nil {
|
|
h.broker.Disconnect()
|
|
}
|
|
|
|
h.started = false
|
|
}
|
|
|
|
// Call invokes a service method
|
|
func (h *Harness) Call(endpoint string, req, rsp interface{}) error {
|
|
return h.CallContext(context.Background(), endpoint, req, rsp)
|
|
}
|
|
|
|
// CallContext invokes a service method with context
|
|
func (h *Harness) CallContext(ctx context.Context, endpoint string, req, rsp interface{}) error {
|
|
if !h.started {
|
|
return fmt.Errorf("harness not started, call Start() first")
|
|
}
|
|
|
|
request := h.client.NewRequest(h.name, endpoint, req)
|
|
return h.client.Call(ctx, request, rsp)
|
|
}
|
|
|
|
// Client returns the test client for advanced usage
|
|
func (h *Harness) Client() client.Client {
|
|
return h.client
|
|
}
|
|
|
|
// Server returns the test server for advanced usage
|
|
func (h *Harness) Server() server.Server {
|
|
return h.server
|
|
}
|
|
|
|
// Registry returns the test registry for advanced usage
|
|
func (h *Harness) Registry() registry.Registry {
|
|
return h.registry
|
|
}
|
|
|
|
// --- Assertions ---
|
|
|
|
// AssertServiceRunning checks that the service is registered
|
|
func (h *Harness) AssertServiceRunning() {
|
|
h.t.Helper()
|
|
|
|
services, err := h.registry.GetService(h.name)
|
|
if err != nil {
|
|
h.t.Errorf("service %s not found: %v", h.name, err)
|
|
return
|
|
}
|
|
if len(services) == 0 || len(services[0].Nodes) == 0 {
|
|
h.t.Errorf("service %s has no running instances", h.name)
|
|
}
|
|
}
|
|
|
|
// AssertCallSucceeds checks that a call succeeds
|
|
func (h *Harness) AssertCallSucceeds(endpoint string, req, rsp interface{}) {
|
|
h.t.Helper()
|
|
|
|
if err := h.Call(endpoint, req, rsp); err != nil {
|
|
h.t.Errorf("call %s failed: %v", endpoint, err)
|
|
}
|
|
}
|
|
|
|
// AssertCallFails checks that a call fails
|
|
func (h *Harness) AssertCallFails(endpoint string, req, rsp interface{}) {
|
|
h.t.Helper()
|
|
|
|
if err := h.Call(endpoint, req, rsp); err == nil {
|
|
h.t.Errorf("expected call %s to fail, but it succeeded", endpoint)
|
|
}
|
|
}
|