如何在C#中通过CallInvoker模拟gRPC调用(无需启动服务)
解决Grpc.Core内存中测试服务的方案
针对你的问题,完全可以通过自定义CallInvoker实现客户端调用直接定向到内存中的服务实例,无需启动真实gRPC服务或使用网络连接,完美解决测试性能、稳定性和并行执行的问题。
核心思路
Grpc.Core的客户端本质是通过CallInvoker接口发起所有调用,替换这个接口的实现,让它直接调用服务实例的业务方法,绕开网络通道的所有开销。这种方式完全在内存中执行,没有端口占用,支持并行测试。
具体实现
1. 自定义DirectServiceCallInvoker
这个类继承自CallInvoker,负责将客户端调用转发到指定的服务实例:
using Grpc.Core; using Grpc.Core.Utils; using System.Reflection; using System.Collections.Generic; using System.Threading.Tasks; public class DirectServiceCallInvoker<TService> : CallInvoker where TService : class { private readonly TService _serviceInstance; public DirectServiceCallInvoker(TService serviceInstance) { _serviceInstance = GrpcPreconditions.CheckNotNull(serviceInstance, nameof(serviceInstance)); } // 处理异步Unary调用(最常用的调用类型) public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>( Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) { // 匹配服务中对应的方法(gRPC生成的服务方法签名为:Task<TResponse> 方法名(TRequest, ServerCallContext)) var serviceMethod = typeof(TService).GetMethod( method.Name, BindingFlags.Public | BindingFlags.Instance, new[] { typeof(TRequest), typeof(ServerCallContext) }); GrpcPreconditions.CheckNotNull(serviceMethod, $"服务类型 {typeof(TService).Name} 未实现方法 {method.Name}"); // 创建模拟的ServerCallContext,满足服务方法的参数要求 var mockContext = new MockServerCallContext(options); // 调用服务实例的方法 var responseTask = (Task<TResponse>)serviceMethod.Invoke(_serviceInstance, new object[] { request, mockContext }); // 包装成gRPC客户端期望的AsyncUnaryCall对象 return new AsyncUnaryCall<TResponse>( responseTask, Task.FromResult(new Metadata()), () => mockContext.Status, () => mockContext.ResponseTrailers, () => { }); } // 如需支持流式调用,补充以下方法的实现(示例以ServerStreaming为例) public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) { var serviceMethod = typeof(TService).GetMethod( method.Name, BindingFlags.Public | BindingFlags.Instance, new[] { typeof(TRequest), typeof(IServerStreamWriter<TResponse>), typeof(ServerCallContext) }); GrpcPreconditions.CheckNotNull(serviceMethod, $"服务类型 {typeof(TService).Name} 未实现流式方法 {method.Name}"); var mockContext = new MockServerCallContext(options); var responseStream = new MockServerStreamWriter<TResponse>(); var invokeTask = serviceMethod.Invoke(_serviceInstance, new object[] { request, responseStream, mockContext }) as Task; return new AsyncServerStreamingCall<TResponse>( responseStream.AsAsyncStreamReader(), Task.FromResult(new Metadata()), () => mockContext.Status, () => mockContext.ResponseTrailers, () => { }); } // 省略ClientStreaming、DuplexStreaming等方法的实现,按需补充 public override AsyncClientStreamingCall<TRequest, TResponse> AsyncClientStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options) { throw new NotImplementedException("如需支持客户端流式调用,请补充实现此方法"); } public override AsyncDuplexStreamingCall<TRequest, TResponse> AsyncDuplexStreamingCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options) { throw new NotImplementedException("如需支持双向流式调用,请补充实现此方法"); } public override TResponse BlockingUnaryCall<TRequest, TResponse>(Method<TRequest, TResponse> method, string host, CallOptions options, TRequest request) { return AsyncUnaryCall(method, host, options, request).ResponseAsync.Result; } } // 模拟ServerCallContext的基础实现 public class MockServerCallContext : ServerCallContext { private readonly CallOptions _callOptions; public MockServerCallContext(CallOptions callOptions) { _callOptions = callOptions; } public override CancellationToken CancellationToken => _callOptions.CancellationToken; public override string Host => "localhost"; public override string Peer => "test-client"; public override Metadata RequestHeaders => _callOptions.Headers ?? new Metadata(); public override Metadata ResponseTrailers { get; } = new Metadata(); public override Status Status { get; set; } = Status.DefaultSuccess; public override Task WriteResponseHeadersAsync(Metadata responseHeaders) => Task.CompletedTask; } // 模拟ServerStreamWriter的基础实现(用于流式调用) public class MockServerStreamWriter<T> : IServerStreamWriter<T> { private readonly List<T> _messages = new List<T>(); public List<T> WrittenMessages => _messages; public Task WriteAsync(T message) { _messages.Add(message); return Task.CompletedTask; } public WriteOptions WriteOptions { get; set; } // 转换为AsyncStreamReader供客户端使用 public IAsyncStreamReader<T> AsAsyncStreamReader() { return new MockAsyncStreamReader<T>(_messages); } } public class MockAsyncStreamReader<T> : IAsyncStreamReader<T> { private readonly IEnumerator<T> _enumerator; public MockAsyncStreamReader(IEnumerable<T> messages) { _enumerator = messages.GetEnumerator(); } public T Current => _enumerator.Current; public Task<bool> MoveNext(CancellationToken cancellationToken) { return Task.FromResult(_enumerator.MoveNext()); } }
2. 在测试中使用
直接创建服务实例(可以是真实实现或Mock对象),用自定义CallInvoker初始化客户端,即可实现内存调用:
// 1. 创建服务实例(这里用真实业务实现,也可以用Moq等工具Mock) var greeterService = new GreeterService(); // 2. 初始化自定义CallInvoker var callInvoker = new DirectServiceCallInvoker<Greeter.GreeterBase>(greeterService); // 3. 用CallInvoker创建客户端(替代原来的Channel方式) var client = new Greeter.GreeterClient(callInvoker); // 4. 执行测试调用,完全在内存中执行,无网络开销 var request = new HelloRequest { Name = "TestUser" }; var reply = await client.SayHelloAsync(request); // 5. 断言结果 Assert.AreEqual("Hello TestUser", reply.Message);
方案优势
- 性能提升:完全内存调用,比网络调用快几个数量级,大幅缩短测试执行时间
- 稳定性高:避免本地网络波动、端口占用等问题导致的测试失败
- 支持并行测试:无需绑定端口,多个测试用例可以同时执行,不会有资源冲突
- 灵活可控:可以直接Mock服务实例,注入测试依赖,轻松模拟各种业务场景
注意事项
- 该方案兼容Grpc.Core 2.30.0版本,
CallInvoker在该版本中是公开可继承的 - 流式调用(Server/Client/Duplex)需要补充对应方法的实现,示例中提供了ServerStreaming的基础实现,可按需扩展
MockServerCallContext可以根据测试需求扩展更多属性(比如模拟认证信息、自定义请求头等)
内容的提问来源于stack exchange,提问作者Rafael
相关产品推荐
相关产品推荐

