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

如何为基于Discord.Net的Discord机器人编写单元测试

Discord.Net场景下CommandHandler的TDD单元测试实现

现有代码的核心问题

  • 框架特性适配不足:Discord.Net的所有平台实体(消息、用户、频道等)都遵循不可变设计原则,对应接口的所有属性仅定义get访问器,不存在set访问器,本身就无法通过赋值逻辑修改属性值
  • Moq语法使用错误:针对接口中仅定义get访问器的只读属性,直接用赋值式的lambda做Setup属于语法误用,Moq需要通过SetupGet方法配置get访问器的返回值,而非尝试为属性设置值
  • 类型匹配错误:IMessage.Author的实际类型是IUser接口,不是字符串,直接传入"bot"字符串无法通过类型检查
  • 模拟逻辑的层级缺失:没有逐层模拟依赖链(消息作者→机器人自身ID),无法完成“机器人自身发的消息被忽略”这个核心场景的校验

规范的测试实现方案

1. 前置设计优化

从可测试性角度调整依赖签名:OnMessageReceivedAsync方法的入参直接依赖IMessage接口,而非具体的SocketMessage抽象类,从设计源头避免需要实例化带网络上下文的Socket类型,降低模拟成本。

2. 正确的Moq模拟逻辑

针对只读接口属性,使用SetupGet方法配置get访问器的返回值,而非尝试为属性设置值;按照依赖关系逐层模拟需要的对象,仅配置测试路径上会用到的属性:

  • 模拟DiscordSocketClient实例,预设其CurrentUser(即机器人自身)的固定测试ID
  • 模拟消息发送者IUser对象,设置其ID与机器人自身ID一致,匹配“消息为机器人自己发送”的测试场景
  • 模拟IMessage对象,配置其Author属性返回上面的消息发送者对象

3. 完整测试代码示例

using Discord;
using Discord.Commands;
using Discord.WebSocket;
using Moq;
using Runa.DiscordBot.Infrastructure.CommandHandling;
using Xunit;

namespace Runa.DiscordBot.Infrastructure.Tests
{
    public class CommandHandlerTests
    {
        [Fact]
        public async Task OnMessageReceivedAsync_IgnoresSelfAsCommandIssuer()
        {
            // 预设固定测试常量
            const ulong BotUserId = 123456789UL;
            const ulong MessageAuthorId = BotUserId;

            // 模拟DiscordSocketClient依赖,预设机器人自身ID
            var mockClient = new Mock<DiscordSocketClient>();
            var mockBotUser = new Mock<ISelfUser>();
            mockBotUser.SetupGet(u => u.Id).Returns(BotUserId);
            mockClient.SetupGet(c => c.CurrentUser).Returns(mockBotUser.Object);

            // 模拟消息发送者,设置其ID与机器人ID完全一致
            var mockMessageAuthor = new Mock<IUser>();
            mockMessageAuthor.SetupGet(u => u.Id).Returns(MessageAuthorId);

            // 模拟IMessage对象,配置Author属性返回预设的发送者
            var mockMessage = new Mock<IMessage>();
            mockMessage.SetupGet(m => m.Author).Returns(mockMessageAuthor.Object);

            // 模拟下游命令解析依赖,用于校验调用状态
            var mockCommandService = new Mock<CommandService>();

            // 初始化被测的CommandHandler实例
            var commandHandler = new CommandHandler(mockClient.Object, mockCommandService.Object);

            // 触发被测异步方法
            await commandHandler.OnMessageReceivedAsync(mockMessage.Object);

            // 断言:命令解析逻辑完全未被触发,证明自身消息被正确忽略
            mockCommandService.Verify(
                cs => cs.ExecuteAsync(It.IsAny<ICommandContext>(), It.IsAny<IServiceProvider>()),
                Times.Never
            );
        }
    }
}

4. 注意事项

  • 测试异步方法时需将测试方法标记为async Task,通过await等待方法执行完成后再做断言,避免使用.Result或.Wait()导致的测试上下文死锁
  • 日常测试使用Moq的宽松模式即可,仅配置测试路径上会访问的属性,未配置的属性会返回对应类型的默认值,无需全量配置所有接口成员

可选替代方案

如果不想依赖Moq这类动态模拟框架,可以手写实现最小化的测试替身(Test Double):

  • 自定义FakeMessage类实现IMessage接口,仅将测试需要用到的Author属性返回预设的测试用户对象,接口中其他未用到的成员直接抛出NotImplementedException即可
  • 相比动态模拟,手写的测试替身在简单场景下调试成本更低,也没有第三方模拟框架的版本兼容问题

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.23 05:57:03