使用AddCodeFirstGrpcClient与WebApplicationFactory测试代码优先gRPC服务失败求助
我尝试用Protobuf-net的客户端工厂方法AddCodeFirstGrpcClient,结合WebApplicationFactory<TEntryPoint>在测试环境创建客户端,但第二个测试方法TestViaCodeFirst执行失败,抛出以下错误:
Message:
Grpc.Core.RpcException : Status(StatusCode="Unavailable", Detail="Error connecting to subchannel.", DebugException="System.Net.Sockets.SocketException: No connection could be made because the target machine actively refused it.")
---- System.Net.Sockets.SocketException : No connection could be made because the target machine actively refused it.
Stack Trace:
ConnectionManager.PickAsync(PickContext context, Boolean waitForReady, CancellationToken cancellationToken)
BalancerHttpHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
GrpcCall2.RunCall(HttpRequestMessage request, Nullable1 timeout)
Reshape.UnaryTaskAsyncImpl[TRequest,TResponse](AsyncUnaryCall`1 call, MetadataContext metadata, CancellationToken cancellationToken) line 554
GreeterServiceTests.TestViaCodeFirst() line 50
--- End of stack trace from previous location ---
----- Inner Stack Trace -----
AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)
IValueTaskSource.GetResult(Int16 token)
Socket.
SocketConnectivitySubchannelTransport.TryConnectAsync(ConnectContext context, Int32 attempt)
我的代码
共享代码
using ProtoBuf.Grpc; using System.Runtime.Serialization; using System.ServiceModel; namespace GrpcExperiments.Shared; [DataContract] public class HelloReply { [DataMember(Order = 1)] public string? Message { get; set; } } [DataContract] public class HelloRequest { [DataMember(Order = 1)] public string? Name { get; set; } } [ServiceContract] public interface IGreeterService { [OperationContract] Task<HelloReply> SayHelloAsync(HelloRequest request); }
服务端代码
using ProtoBuf.Grpc; using GrpcExperiments.Shared; public class GreeterService : IGreeterService { public Task<HelloReply> SayHelloAsync(HelloRequest request) { return Task.FromResult( new HelloReply { Message = $"Hello {request.Name}" }); } }
客户端代码
using Grpc.Net.Client; using GrpcExperiments.Shared; using Microsoft.Extensions.DependencyInjection; using ProtoBuf.Grpc.Client; using ProtoBuf.Grpc.ClientFactory; using Gncf = Grpc.Net.ClientFactory; public class Program { public static async Task Main() { await CreateClientOld(); await CreateClientNew(); Console.WriteLine("Press any key to exit..."); Console.ReadKey(); } static async Task CreateClientNew() { var services = new ServiceCollection(); services.AddCodeFirstGrpcClient<IGreeterService>(o => { o.Address = new Uri("https://localhost:7158"); }); using var serviceProvider = services.BuildServiceProvider(); var factory = serviceProvider.GetRequiredService<Gncf.GrpcClientFactory>(); var client = factory.CreateClient<IGreeterService>(nameof(IGreeterService)); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Console.WriteLine($"Greeting (new): {reply.Message}"); } static async Task CreateClientOld() { using var channel = GrpcChannel.ForAddress("https://localhost:7158"); var client = channel.CreateGrpcService<IGreeterService>(); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Console.WriteLine($"Greeting (old): {reply.Message}"); } }
测试代码
using Grpc.Net.Client; using GrpcExperiments.Shared; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using ProtoBuf.Grpc.Client; using ProtoBuf.Grpc.ClientFactory; using Gncf = Grpc.Net.ClientFactory; namespace GrpcExperiements.Test; public class GreeterServiceTests { [Fact] public async Task TestViaGrpcChannel() { using var factory = new WebApplicationFactory<Program>(); using var channel = GrpcChannel.ForAddress(factory.Server.BaseAddress, new GrpcChannelOptions { HttpClient = factory.CreateClient() }); var client = channel.CreateGrpcService<IGreeterService>(); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Assert.Equal("Hello GreeterClient", reply.Message); } [Fact] public async Task TestViaCodeFirst() { using var webApp = new WebApplicationFactory<Program>(); using var services = new ServiceCollection(); services .AddCodeFirstGrpcClient<IGreeterService>(nameof(IGreeterService), clientFactoryOptions => { clientFactoryOptions.Address = webApp.Server.BaseAddress; //clientFactoryOptions.ChannelOptionsActions.Add(options => options.HttpClient = webApp.CreateClient()); }); //.ConfigureChannel(channel => // channel.HttpClient = webApp.CreateClient()); using var serviceProvider = services.BuildServiceProvider(); var factory = serviceProvider.GetRequiredService<Gncf.GrpcClientFactory>(); var client = factory.CreateClient<IGreeterService>(nameof(IGreeterService)); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Assert.Equal("Hello GreeterClient", reply.Message); } }
请问该如何解决这个连接被拒绝的问题?
问题核心是你没有把WebApplicationFactory生成的测试HttpClient绑定到gRPC客户端工厂的通道配置里。WebApplicationFactory的测试服务器不会监听实际网络端口,而是通过内部管道处理请求,默认情况下gRPC客户端工厂会创建独立的HttpClient,无法访问这个内部管道,导致连接被拒绝。
修改TestViaCodeFirst方法,在配置AddCodeFirstGrpcClient时,通过ChannelOptionsActions注入测试用的HttpClient即可解决:
[Fact] public async Task TestViaCodeFirst() { using var webApp = new WebApplicationFactory<Program>(); using var services = new ServiceCollection(); services .AddCodeFirstGrpcClient<IGreeterService>(nameof(IGreeterService), clientFactoryOptions => { clientFactoryOptions.Address = webApp.Server.BaseAddress; // 绑定测试HttpClient到通道配置 clientFactoryOptions.ChannelOptionsActions.Add(options => options.HttpClient = webApp.CreateClient()); }); using var serviceProvider = services.BuildServiceProvider(); var factory = serviceProvider.GetRequiredService<Gncf.GrpcClientFactory>(); var client = factory.CreateClient<IGreeterService>(nameof(IGreeterService)); var reply = await client.SayHelloAsync( new HelloRequest { Name = "GreeterClient" }); Assert.Equal("Hello GreeterClient", reply.Message); }
原理说明
WebApplicationFactory.CreateClient()返回的HttpClient已经配置为直接和测试服务器的内部管道通信,不需要走实际网络端口,这也是第一个测试方法TestViaGrpcChannel能成功的原因。- 使用
AddCodeFirstGrpcClient时必须显式关联这个测试HttpClient,否则客户端会尝试通过网络连接BaseAddress,但测试服务器并未监听该端口,自然会被拒绝。
另外,也可以用ConfigureChannel方法来配置通道的HttpClient,效果完全一致:
services .AddCodeFirstGrpcClient<IGreeterService>(nameof(IGreeterService), o => { o.Address = webApp.Server.BaseAddress; }) .ConfigureChannel(options => options.HttpClient = webApp.CreateClient());
内容的提问来源于stack exchange,提问作者Shimmy Weitzhandler

