如何对派生的 ModuleBase class 进行单元测试?

How to unit test derived ModuleBase class?

我想对派生的一种方法进行单元测试 ModuleBase<SocketCommandContext> class.

有什么好的劫持方法吗属性ModuleBase.Context?

一个选项可能是将其覆盖到 auto-property 并使用派生的 SocketCommandContext class 的假实例分配给它。 SocketCommandContext.User 成员可能需要同样的东西。

也许有更好的方法。

I couldn't find anything useful in project page

谢谢。

public class SampleModule: ModuleBase<SocketCommandContext>
    {
        private readonly IRepository _repository;

        public SocketCommandContext Context { get; set; }

        public SampleModule(
            IRepository _repository)
        {
            _repository= repository;
        }

        [Command("test", RunMode = RunMode.Async)]
        public async Task RunAsync([Summary("user")]SocketGuildUser? targetUser = null)
        {
            var user = targetUser ?? Context.User as SocketGuildUser ?? throw new Exception();
            var result = await _repository.GetAsync(user.Id);
            await ReplyAsync(result.Value);
        }
}
private readonly Mock<IRepository> _repositoryMock = new(MockBehavior.Strict);

 [TestMethod]
 public async Task Should_Reply()
 {
      _repositoryMock
          .Setup(pr => pr.GetAsync(It.IsAny<ulong>()))
          .Verifiable();

     

     var module = new SampleModule(_repositoryMock.Object);
     await module.RunAsync();

     _repositoryMock.Verify();
 }
       

可以通过反射内部方法 IModuleBase.SetContext(ICommandContext) 调用并从参数分配新上下文来模拟 ModuleBase.Context

private void SetContext(SampleModule module)
{
    var setContext = module.GetType().GetMethod(
        "Discord.Commands.IModuleBase.SetContext",
        BindingFlags.NonPublic | BindingFlags.Instance);
    setContext.Invoke(_module, new object[] { _commandContextMock.Object });
}

至于SocketGuildUser,可以使用需要SocketGuildSocketGlobalUser的内部ctor。它们还需要用反射实例化,需要其他依赖。

SocketGlobalUser 是一个艰难的定义,因为 class 定义甚至是内部的。

另一种选择是将 Socket* 类型切换为它们实现的接口。

SocketGuildUser 会变成 IGuildUserIUser

实例化 Socket 类型的示例方法

    public static object CreateSocketGlobalUser(DiscordSocketClient discordSocketClient, ulong id)
        {
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
            var discordNetAssembly = assemblies
                .FirstOrDefault(pr => pr.FullName == "Discord.Net.WebSocket, Version=2.4.0.0, Culture=neutral, PublicKeyToken=null");

            var socketGlobalUserType = discordNetAssembly.GetType("Discord.WebSocket.SocketGlobalUser");


            var socketGlobalUserCtor = socketGlobalUserType.GetConstructor(
                BindingFlags.NonPublic | BindingFlags.Instance,
                null, new[]{
                        typeof(DiscordSocketClient),
                        typeof(ulong),
                    }, null);

            var parameters = new object[] {
                discordSocketClient, id
            };

            var socketGlobalUser = socketGlobalUserCtor.Invoke(parameters);
            return socketGlobalUser;
        }

public static SocketGuild CreateSocketGuild(DiscordSocketClient discordSocketClient, ulong id)
        {
            var bindingAttr = BindingFlags.NonPublic | BindingFlags.Instance;
            var socketGuildCtor = typeof(SocketGuild).GetConstructor(
             bindingAttr,
             null, new[]{
                    typeof(DiscordSocketClient),
                    typeof(ulong),
             }, null);

            var socketGuild = (SocketGuild)socketGuildCtor.Invoke(new object[] {
                discordSocketClient, id,
            });
            return socketGuild;
        }

public static SocketGuildUser CreateSocketGuildUser(SocketGuild socketGuild, object socketGlobalUser)
        {
            var bindingAttr = BindingFlags.NonPublic | BindingFlags.Instance;
            var types = new[]{
                typeof(SocketGuild),
                socketGlobalUser.GetType(),
            };
            var socketGuildUserCtor = typeof(SocketGuildUser).GetConstructor(
               bindingAttr,
               null, types, null);

            var parameters = new object[] {
                socketGuild, socketGlobalUser
            };

            var socketGuildUser = (SocketGuildUser)socketGuildUserCtor.Invoke(parameters);

            return socketGuildUser;
        }

ReplyAsync 方法在内部调用 Context.Channel.SendMessageAsync 并且通过一些工作它也可以被模拟。

private readonly Mock<ICommandContext> _commandContextMock = new(MockBehavior.Strict);
private readonly Mock<IMessageChannel> _messageChannelMock = new(MockBehavior.Strict);
private readonly Mock<IUserMessage> _userMessageMock = new(MockBehavior.Strict);

_commandContextMock
    .Setup(pr => pr.Channel)
    .Returns(_messageChannelMock.Object);

_messageChannelMock
    .Setup(pr => pr.SendMessageAsync(
        It.IsAny<string>(),
        It.IsAny<bool>(),
        It.IsAny<Embed>(),
        It.IsAny<RequestOptions>(),
        It.IsAny<AllowedMentions>(),
        It.IsAny<MessageReference>()))
    .ReturnsAsync(_userMessageMock.Object);

上面的例子测试。

private readonly Mock<ICommandContext> _commandContextMock = new(MockBehavior.Strict);
private readonly Mock<IMessageChannel> _messageChannelMock = new(MockBehavior.Strict);
private readonly Mock<IUserMessage> _userMessageMock = new(MockBehavior.Strict);
private readonly Mock<IRepository> _repositoryMock = new(MockBehavior.Strict);

 [TestMethod]
 public async Task Should_Reply()
 {
      _repositoryMock
          .Setup(pr => pr.GetAsync(It.IsAny<ulong>()))
          .Verifiable(); 

      var discordSocketClientMock = new Mock<DiscordSocketClient>(MockBehavior.Strict);
      var socketGlobalUser = CreateSocketGlobalUser(discordSocketClientMock.Object, 1);
      var socketGuild = CreateSocketGuild(discordSocketClientMock.Object, 1);
      var socketGuildUser = CreateSocketGuildUser(socketGuild, socketGlobalUser);

      _commandContextMock
          .Setup(pr => pr.Channel)
          .Returns(_messageChannelMock.Object);

      _commandContextMock
          .Setup(pr => pr.User)
          .Returns(socketGuildUser);

      _messageChannelMock
          .Setup(pr => pr.SendMessageAsync(
              It.IsAny<string>(),
              It.IsAny<bool>(),
              It.IsAny<Embed>(),
              It.IsAny<RequestOptions>(),
              It.IsAny<AllowedMentions>(),
              It.IsAny<MessageReference>()))
          .ReturnsAsync(_userMessageMock.Object);

     var module = new SampleModule(_repositoryMock.Object);
     SetContext(_commandContextMock.Object);


     await module.RunAsync();

     _repositoryMock.Verify();
 }

一些部分可以提取到测试库 class 由设置代码的大小决定。