如何从外部消费者调用的依赖项模拟任务

问题描述 投票:0回答:1

我是模拟新手,被要求为一个方法构建一些单元测试,到目前为止我所理解的是,该类的一个依赖项(要测试的方法所在的位置)等待消费者调用任务<> 并且该方法为其分配了一些工作。

要测试的类 - 方法 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);
        }
    }
}
c# unit-testing moq mstest
1个回答
0
投票

棘手,棘手。虽然您可以使用 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

© www.soinside.com 2019 - 2024. All rights reserved.