C#单元测试GoogleCloudVision服务:注入ImageAnnotatorClient实现Mock
解答
完全可以通过注入ImageAnnotatorClient的方式完成Mock,你当前的实现因为在构造函数内部硬编码初始化客户端,无法直接替换依赖做单元测试,只需要做小幅代码改造即可实现隔离测试。
现有实现的核心问题
ImageAnnotatorClient实例在构造函数内部通过ImageAnnotatorClientBuilder直接创建,测试时没有入口替换成Mock实例,运行测试会直接调用真实的Google Cloud Vision接口,既会产生外部调用开销,也无法控制接口返回结果覆盖所有测试分支。
改造方案
不需要修改原有业务逻辑,只需要新增一个支持传入ImageAnnotatorClient的构造函数,供测试时注入Mock实例即可,生产环境仍然用原有逻辑自动创建客户端:
public class GoogleCloudVisionService : IGoogleCloudVision { private readonly GoogleCloudVisionSettings googleCloudVisionSettings; private readonly ImageAnnotatorClient client; // 生产环境使用的构造函数,通过配置自动创建真实客户端 public GoogleCloudVisionService(IOptions<GoogleCloudVisionSettings> googleCloudVisionSettings) { this.googleCloudVisionSettings = googleCloudVisionSettings.Value.WithParsedKey(); var credential = GoogleCredential.FromJson(this.googleCloudVisionSettings.ApiJson); ImageAnnotatorClientBuilder clientBuilder = new ImageAnnotatorClientBuilder(); clientBuilder.Credential = credential; client = clientBuilder.Build(); } // 测试场景使用的构造函数,支持直接传入Mock客户端 public GoogleCloudVisionService(IOptions<GoogleCloudVisionSettings> googleCloudVisionSettings, ImageAnnotatorClient client) { this.googleCloudVisionSettings = googleCloudVisionSettings.Value.WithParsedKey(); this.client = client; } // 原有业务方法保持不变 public async Task<SafeSearchAnnotation> GetImageAnnotation(byte[] imageBytes) { var image = Image.FromBytes(imageBytes); var annotation = await client.DetectSafeSearchAsync(image); return annotation; } public async Task<bool> IsImageSafe(byte[] imageBytes) { var annotations = await GetImageAnnotation(imageBytes); var isSafe = annotations.Adult < googleCloudVisionSettings.Annotations.Adult && annotations.Racy < googleCloudVisionSettings.Annotations.Racy && annotations.Violence < googleCloudVisionSettings.Annotations.Violence && annotations.Medical < googleCloudVisionSettings.Annotations.Medical && annotations.Spoof < googleCloudVisionSettings.Annotations.Spoof; return isSafe; } }
如果使用.NET默认依赖注入容器,注册服务时建议用工厂模式显式指定调用生产环境构造函数,避免容器尝试自动解析ImageAnnotatorClient导致报错:
// 生产环境DI注册示例 services.AddOptions<GoogleCloudVisionSettings>().Bind(configuration.GetSection("GoogleCloudVision")); services.AddSingleton<IGoogleCloudVision, GoogleCloudVisionService>(sp => { var settings = sp.GetRequiredService<IOptions<GoogleCloudVisionSettings>>(); return new GoogleCloudVisionService(settings); });
单元测试Mock示例
以Moq框架为例,ImageAnnotatorClient的公开方法均为虚方法,可以直接Mock重写返回值,不需要额外封装包装层:
[Fact] public async Task IsImageSafe_ShouldReturnFalse_WhenAdultScoreExceedsThreshold() { // 准备测试配置 var mockSettings = Options.Create(new GoogleCloudVisionSettings { Annotations = new AnnotationThresholdConfig { Adult = Likelihood.Possible, Racy = Likelihood.Possible, Violence = Likelihood.Possible, Medical = Likelihood.Possible, Spoof = Likelihood.Possible } }); // 构造Mock客户端,指定接口返回结果 var mockVisionClient = new Mock<ImageAnnotatorClient>(); mockVisionClient.Setup(c => c.DetectSafeSearchAsync( It.IsAny<Image>(), It.IsAny<ImageContext>(), It.IsAny<CancellationToken>())) .ReturnsAsync(new SafeSearchAnnotation { Adult = Likelihood.Likely, // 分值超过设定阈值 Racy = Likelihood.Unlikely, Violence = Likelihood.Unlikely, Medical = Likelihood.Unlikely, Spoof = Likelihood.Unlikely }); // 注入Mock实例创建测试服务 var testService = new GoogleCloudVisionService(mockSettings, mockVisionClient.Object); // 执行测试逻辑 var checkResult = await testService.IsImageSafe(new byte[] { 0x00 }); // 断言结果符合预期 Assert.False(checkResult); }
Mock
DetectSafeSearchAsync方法时注意匹配方法重载签名,该方法默认包含ImageContext和CancellationToken两个可选参数,Setup时需要把这两个参数也用It.IsAny<>()匹配,否则会出现Mock逻辑不生效的问题。
内容的提问来源于stack exchange,提问作者Rashid
相关产品推荐
相关产品推荐

