我是模拟新手,被要求为一个方法构建一些单元测试,到目前为止我所理解的是,该类的一个依赖项(要测试的方法所在的位置)等待消费者调用任务<> 并且该方法为其分配了一些工作。
要测试的类 - 方法 StartProcess
public class MessageProcessor
{
private readonly IMessageReceiver _receiver;
public MessageProcessor(IMessageReceiver receiver)
{
_receiver = receiver;
}
public async void StartProcess(Func<string, string, Task<bool>> funcToBeExecutedInTheCaller)
{
await _receiver.OnMessageReceived<object>("string param", async (messageReceived, cancelationToken) =>
{
// Some validations to the messageReceived object
var callerParam1 = string.Empty;
var callerParam2 = string.Empty;
var result = await funcToBeExecutedInTheCaller(callerParam1, callerParam2);
// validate result and do some more work
});
}
}
我需要做的是构建一些单元测试,其中调用 StartProcess 方法,并且也可以使用不同的数据调用 OnMessageReceived 任务,因为我无权访问正在执行 funcToBeExecutedInTheCaller 代码的 dll,这会很好如果我可以编写要在测试中执行的自定义方法,或者直接返回自定义 Result 对象。
为此,我使用 MSTest 和 Moq。
不幸的是,我很失落,因为我以前没有嘲笑的经验。这就是我到目前为止所做的
[TestClass()]
public class MessageProcessorTest
{
private Mock<IMessageReceiver> receiverMock = null!;
[TestInitialize]
public void TestInitialize()
{
receiverMock = new Mock<IMessageReceiver>();
}
[TestMethod()]
public async Task On_Received_Message()
{
receiverMock.Setup(o => o.OnMessageReceived(“some string Param”, It.IsAny<DataObject>())).Returns(Task.Run(() => { }));
consumer = new MessageProcessor(receiverMock.Object);
try
{
consumer.StartProcess(...) //No idea how to execute it here
}
catch (Exception ex)
{
Assert.Fail("No exception expected: " + ex.Message);
}
}
}
棘手,棘手。虽然您可以使用 Moq 进行测试,但我认为使用 Test Spy 更简洁,因此我提供了两种替代方案。
我冒昧地在 xUnit.net 而不是 MSTest 中编写测试,但我相信您可以将
[Fact]
属性替换为 [TestMethod]
。
这里有一种使用 Moq 的方法:
[Fact]
public void OnMessageReceivedMock()
{
var receiverMock = new Mock<IMessageReceiver>();
receiverMock
.Setup(r => r.OnMessageReceived<object>(
"string param",
It.IsAny<Func<object, CancellationToken, Task>>()))
.Callback<string, Func<object, CancellationToken, Task>>(
async (stringParam, f) =>
await f("messageReceived", new CancellationToken()))
.Returns(Task.CompletedTask)
.Verifiable();
var sut = new MessageProcessor(receiverMock.Object);
sut.StartProcess(
async (callerParam1, callerParam2) =>
{
Assert.Equal("", callerParam1);
Assert.Equal("", callerParam2);
return await Task.FromResult(true);
});
receiverMock.Verify();
}
真的很棘手。我必须首先
Setup
receiverMock
- 不是因为测试确实需要设置返回值,而是因为这是访问 Callback
方法的唯一方法。
每次 Callback
匹配时,Moq 都会调用您提供给
Setup
的表达式。在该回调中,测试然后调用 f
函数 - 即 funcToBeExecutedInTheCaller
。如果 MessageProcessor
正确实现,带有断言的匿名代码块将运行,并验证参数是否符合预期。
测试然后调用
StartProcess
,最后调用 receiverMock.Verify()
。因为 Setup
管道以 Verifiable()
结束,所以测试不必重复 Setup
中已有的表达式; Verify()
就够了。
在我看来,以这种方式使用 Moq 太复杂了,所以我想建议一个基于 Test Spy 的更简单的替代方案。
首先,在测试代码中定义一个测试间谍:
internal sealed class SpyMessageReceiver :
Collection<(string, Func<object, CancellationToken, Task>)>,
IMessageReceiver
{
public Task OnMessageReceived<T>(
string stringParam,
Func<object, CancellationToken, Task> value)
{
Add((stringParam, value));
return Task.CompletedTask;
}
}
这只是一个继承自集合基类的类,并且还实现了
IMessageReceiver
。每次有人调用 OnMessageReceived
时,Spy 都会使用基类的 Add
方法来保存调用参数以供以后使用。
完成后,测试可能仍然需要一些时间来理解,但至少看起来相当漂亮:
[Fact]
public async Task OnMessageReceivedSpy()
{
var receiverSpy = new SpyMessageReceiver();
var sut = new MessageProcessor(receiverSpy);
sut.StartProcess(
async (callerParam1, callerParam2) =>
{
Assert.Equal("", callerParam1);
Assert.Equal("", callerParam2);
return await Task.FromResult(true);
});
var (sp, f) = Assert.Single(receiverSpy);
Assert.Equal("string param", sp);
await f("messageReceived", new CancellationToken());
}
请注意,我们将一个匿名代码块传递给
StartProcess
- 它扮演 funcToBeExecutedInTheCaller
的角色。该代码块内部有一些断言,因为我假设您真正想要验证的是使用正确的参数调用 funcToBeExecutedInTheCaller
。
然而,此时
funcToBeExecutedInTheCaller
还没有被调用。它仅在 _receiver
决定运行时运行。
幸好我们有间谍。
首先,我们断言
receiverSpy
实际上有一组参数,这意味着 OnMessageReceived
被调用了。 (IIRC,我不确定 MSTest 是否有类似于 Assert.Single
的内容 - 如果没有,您必须首先断言 receiverSpy
包含单个值,然后提取该值。)
为了更好地衡量,测试首先验证
OnMessageReceived
是用 "string param"
调用的 - 我不知道,但这可能很重要。
最后,测试调用spy记录的函数。然后运行匿名代码块,然后运行断言。
重要的是,我发现这两个测试都因更改断言或破坏
StartProcess
实现而失败。这很重要,因为它告诉我测试实际上发现了被测系统 (SUT) 中的错误。
虽然我更喜欢基于 Spy 的测试,但我仍然认为它太复杂了。我知道这不是测试驱动开发 (TDD),而是测试后开发,但 TDD 的一个重要教训是,测试告诉您客户端代码使用 SUT 是多么容易。
关于 API 设计反馈的教训也适用于在实现代码之后编写的测试。在我看来,这两个测试都太复杂了。我认为这是一个让 API 更简单的强烈建议。
最后,除非你绝对必须使用它,避免 async void。