如何为基于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
相关产品推荐
相关产品推荐

