如何在C#中测试gRPC拦截器?.NET Core实践遇测试难题
解决gRPC拦截器单元测试中continuation参数模拟难题的经历
我最近维护着一个同时提供REST和gRPC接口的.NET Core应用,原本用JWT中间件处理认证授权一直很顺畅。后来需要给gRPC接口加上ApiKey(GUID格式)和客户密钥的验证逻辑,于是写了个AuthorizeApplicationKeyInterceptor拦截器,用来拦截所有UnaryServerHandler调用,通过反射读取服务方法上的AuthorizeApplicationKeyAttribute来做权限校验。
功能在生产环境跑起来没问题,但一到单元测试环节就卡壳了——怎么都没法正确模拟UnaryServerMethod<TRequest, TResponse>类型的continuation参数。核心问题出在拦截器里依赖continuation.Target.GetType()获取服务类型的逻辑:
- 实际客户端调用时,
continuation.Target是ResolvedInterceptorInvoker类型,我能通过它的泛型参数拿到正确的服务类型 - 但单元测试时,模拟出来的continuation的Target直接就是被调用的方法本身,原来的泛型参数解析逻辑完全走不通
我试了两种方案都没搞定:
- 用Moq这类框架直接模拟continuation对象,但不管怎么配置,Target的类型都和生产环境不一致,反射逻辑总是抛出异常
- 搭建测试用的gRPC服务器,想模拟真实调用场景,但还是没解决Target类型不匹配的问题
折腾了好一阵后,我决定换个思路——放弃拦截器方案,改用中间件来实现相同的ApiKey验证逻辑。中间件可以直接从HttpContext里获取gRPC的请求信息,而且单元测试起来要简单得多,不用再纠结continuation的Target类型问题。
附上我原来写的拦截器代码,供大家参考:
internal class AuthorizeApplicationKeyInterceptor : Interceptor { /// <summary> /// Will find and return all AuthorizeApplicationKeyAttribute that are set on the method. /// If more than one attribute is set, that means that the application must have at least one role for each attribute. /// Else the application only needs to have one of the roles in the attribute. /// </summary> /// <param name="serviceType">The service where we will get the method from</param> /// <param name="methodName">The method where we will find the attributes.</param> /// <param name="inputParameterTypes"></param> /// <returns>If no attributes exist on the method; null is returned, else all attributes.</returns> private AuthorizeApplicationKeyAttribute[] GetAuthorizeApplicationKeyAttributeForMethod(Type serviceType, string methodName, Type[] inputParameterTypes) { MethodInfo methodInfo = serviceType.GetMethod(methodName, inputParameterTypes); var attributes = methodInfo?.GetCustomAttributes(typeof(AuthorizeApplicationKeyAttribute), true); if (attributes == null || attributes.Length == 0) return null; return attributes as AuthorizeApplicationKeyAttribute[]; } public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>( TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation) { var targetType = continuation.Target.GetType(); // Get the service type for the service we're calling a method in. var serviceType = targetType.GenericTypeArguments[0]; // Get the method name by splitting the context.Method and get the last part that should be the method name. var method = context.Method; var methodSplit = method.Split('/'); var requestedMethodName = methodSplit.Last(); // Get the input type for the method so we find the correct method even if we've two methods that are called the same but have different input parameters. Type[] types = new List<Type>() { targetType.GenericTypeArguments[1], typeof(ServerCallContext) }.ToArray(); // Get the attributes for the method. If null is returned then no attributes were found. var attributes = GetAuthorizeApplicationKeyAttributeForMethod(serviceType, requestedMethodName, types); if (attributes != null) { var httpContext = context.GetHttpContext(); var applicationKey = context.ApplicationKey(); // Validate that the application key has the expected roles. // If we have multiple attributes then the application must have at least one of the roles for every attribute. foreach (AuthorizeApplicationKeyAttribute attribute in attributes) { await attribute.ValidateApplicationKey(httpContext, applicationKey); } } return await continuation(request, context); } }
内容的提问来源于stack exchange,提问作者mklauser
相关产品推荐
相关产品推荐

