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

无接口无虚方法的类如何注入伪造实现?以DiscordSocketClient测试为例

解决方案

方案1:自定义接口+适配包装类(最推荐,长期可维护)

这是.NET生态下处理不可Mock第三方依赖的标准最佳实践,无额外依赖,符合依赖倒置原则,改造成本极低。

实现步骤

  1. 定义包含所需方法的抽象接口
public interface IDiscordClient
{
    Task LoginAsync(TokenType tokenType, string token, bool validateToken = true);
    Task StartAsync();
    Task LogoutAsync();
    Task StopAsync();
}
  1. 编写适配类继承原生DiscordSocketClient并实现接口,直接透传原生方法即可
public class DiscordSocketClientAdapter : DiscordSocketClient, IDiscordClient
{
    // 透传原生类的所有构造函数即可,根据你实际使用的构造函数调整
    public DiscordSocketClientAdapter() : base() {}
    public DiscordSocketClientAdapter(DiscordSocketConfig config) : base(config) {}
}
  1. 修改消费类的依赖为抽象接口,不需要改动原有业务逻辑
public sealed class ConsumingClass
{
    private readonly IDiscordClient _discordClient;
    
    public ConsumingClass(IDiscordClient discordClient)
    {
        _discordClient = discordClient;
    }
    
    public override async Task Initialize(CancellationToken cancellationToken)
    {
        await _discordClient.LoginAsync(TokenType.Bot, "my-token");
        await _discordClient.StartAsync();
    }
    
    public override async Task TearDown(CancellationToken cancellationToken)
    {
        await _discordClient.LogoutAsync();
        await _discordClient.StopAsync();
    }
}
  1. 调整DI容器注册逻辑,将适配类注册为单例
// 替换原来的DiscordSocketClient注册即可
services.AddSingleton<IDiscordClient, DiscordSocketClientAdapter>(sp => 
{
    // 按你原有逻辑构造DiscordSocketClient实例即可
    var config = new DiscordSocketConfig { /* 原有配置 */ };
    return new DiscordSocketClientAdapter(config);
});

测试用例编写

直接Mock抽象接口即可实现你需要的调用校验逻辑

public class ConsumingClassTests
{
    [Fact]
    public async Task Initialize_ShouldInvokeLoginAndStartOnce()
    {
        //  Arrange
        var mockDiscordClient = new Mock<IDiscordClient>();
        var consumingClass = new ConsumingClass(mockDiscordClient.Object);

        // Act
        await consumingClass.Initialize(CancellationToken.None);

        // Assert
        mockDiscordClient.Verify(x => x.LoginAsync(TokenType.Bot, "my-token", true), Times.Once);
        mockDiscordClient.Verify(x => x.StartAsync(), Times.Once);
    }

    [Fact]
    public async Task TearDown_ShouldInvokeLogoutAndStopOnce()
    {
        //  Arrange
        var mockDiscordClient = new Mock<IDiscordClient>();
        var consumingClass = new ConsumingClass(mockDiscordClient.Object);

        // Act
        await consumingClass.TearDown(CancellationToken.None);

        // Assert
        mockDiscordClient.Verify(x => x.LogoutAsync(), Times.Once);
        mockDiscordClient.Verify(x => x.StopAsync(), Times.Once);
    }
}

方案2:免改业务代码的隔离框架(仅适合临时场景)

如果你完全不想改动现有业务代码和DI配置,可以使用支持拦截非虚方法的测试隔离框架,比如Pose、JustMock、TypeMock等,这类框架可以在运行时替换原生方法的实现,不需要定义抽象层。
以Pose为例的测试实现如下:

[Fact]
public async Task Initialize_ShouldCallLoginAndStartOnce()
{
    int loginCallCount = 0;
    int startCallCount = 0;

    // 替换原生LoginAsync实现
    var loginShim = Shim.Replace(() => Is.A<DiscordSocketClient>().LoginAsync(Is.A<TokenType>(), Is.A<string>(), Is.A<bool>()))
        .With((DiscordSocketClient _, TokenType type, string token, bool validate) => 
        {
            loginCallCount++;
            return Task.CompletedTask;
        });
    
    // 替换原生StartAsync实现
    var startShim = Shim.Replace(() => Is.A<DiscordSocketClient>().StartAsync())
        .With((DiscordSocketClient _) => 
        {
            startCallCount++;
            return Task.CompletedTask;
        });

    // 执行测试逻辑
    var client = new DiscordSocketClient();
    var consumingClass = new ConsumingClass(client);
    await PoseContext.Isolate(async () => 
    {
        await consumingClass.Initialize(CancellationToken.None);
    }, loginShim, startShim);

    // 校验调用次数
    Assert.Equal(1, loginCallCount);
    Assert.Equal(1, startCallCount);
}

该方案缺点明显:存在额外第三方依赖,部分框架为商业收费,.NET版本或第三方包更新后容易出现兼容性问题,长期维护成本远高于方案1。

选择建议

优先选择方案1,整体改造成本极低,且后续如果需要升级Discord.Net版本、替换SDK实现都不需要修改上层业务代码,可扩展性极强。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.24 18:24:05