【问题标题】:How to unit test derived ModuleBase class?如何对派生的 ModuleBase 类进行单元测试?
【发布时间】:2021-12-10 00:58:02
【问题描述】:

我想在派生的ModuleBase<SocketCommandContext> 类中对一种方法进行单元测试。

有没有劫持财产ModuleBase.Context的好方法?

一个选项可能是将其覆盖到auto-property 并为其分配派生SocketCommandContext 类的假实例。 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();
 }
       

【问题讨论】:

  • 您不应该对命令本身进行单元测试,而应该对它使用的组件进行单元测试。 DNet 对测试不是很友好,并且可能不值得付出所有必要的抽象所需的努力。如果您的命令使用服务,您可以确保这些服务类是可测试的。

标签: c# discord.net


【解决方案1】:

可以通过反射内部方法调用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,可以使用内部ctor,这需要SocketGuildSocketGlobalUser。它们还需要通过反射进行实例化,并且需要其他依赖项。

SocketGlobalUser 是一个棘手的问题,因为类定义甚至是内部的。

另一种选择是将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();
 }

根据设置代码的大小,可以提取一些部分来测试基类。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-09-17
    • 1970-01-01
    • 2011-12-19
    • 2014-10-08
    • 2011-03-24
    • 2012-02-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多