You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
}

MockDetectSafeSearchAsync方法时注意匹配方法重载签名,该方法默认包含ImageContext和CancellationToken两个可选参数,Setup时需要把这两个参数也用It.IsAny<>()匹配,否则会出现Mock逻辑不生效的问题。

内容的提问来源于stack exchange,提问作者Rashid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 09:33:28