如何对派生的 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
,可以使用需要SocketGuild
和SocketGlobalUser
的内部ctor
。它们还需要用反射实例化,需要其他依赖。
SocketGlobalUser
是一个艰难的定义,因为 class 定义甚至是内部的。
另一种选择是将 Socket*
类型切换为它们实现的接口。
像 SocketGuildUser
会变成 IGuildUser
或 IUser
。
实例化 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 由设置代码的大小决定。
我想对派生的一种方法进行单元测试 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
,可以使用需要SocketGuild
和SocketGlobalUser
的内部ctor
。它们还需要用反射实例化,需要其他依赖。
SocketGlobalUser
是一个艰难的定义,因为 class 定义甚至是内部的。
另一种选择是将 Socket*
类型切换为它们实现的接口。
像 SocketGuildUser
会变成 IGuildUser
或 IUser
。
实例化 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 由设置代码的大小决定。