简体   繁体   中英

How to unit test derived ModuleBase class?

I would like to unit test one method in derived ModuleBase<SocketCommandContext> class.

Is there a good way to hijack property ModuleBase.Context ?

One option might be overriding it to auto-property and assign it with fake instance of derived SocketCommandContext class. The same thing might be needed for SocketCommandContext.User member.

Perhaps there is better way.

I couldn't find anything useful in project page

Thanks.

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();
 }
       

It is possible to mock ModuleBase.Context by invoking via reflection internal method IModuleBase.SetContext(ICommandContext) with assigns new context from parameter.

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 });
}

As for SocketGuildUser , one can use internal ctor which requires SocketGuild and SocketGlobalUser . They also need to be instantiated with reflection and need other dependencies.

SocketGlobalUser was a tough one as the class definition is even internal.

Another option is to switch Socket* types to interfaces which they implement.

Like SocketGuildUser would become IGuildUser or IUser .

Example methods which instantiate Socket types

    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 method internally calls Context.Channel.SendMessageAsync and with a bit of work it can also be mocked.

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);

The example test with things above.

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();
 }

Some parts could be extracted to test base class given by how big the setup code is.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM