试图理解MockSequence

本文关键字:MockSequence | 更新日期: 2023-09-27 18:12:44

我目前正在编写一个应用程序,为了测试它的正确行为,我需要验证方法是否按给定的顺序调用。

对于我的单元测试,我使用xUnit和Moq

现在,为什么我需要测试调用的顺序?

我正在开发一个在不同线程上执行任务的解决方案。只要一个任务被执行,我就会向给定的日志记录器写入一条消息,因此通过检查对日志记录器的调用顺序,我可以确保我的代码被正确实现。

看这里的代码,我试图使用:

public class SchedulerFixture
{
    #region Constructors
    public SchedulerFixture()
    {
        LoggerMock = new Mock<ILogger>(MockBehavior.Strict);
        // Setup of other mocks removed for simplicity.
    }
    #endregion
}
public class SequentialTaskExecutorMock : SchedulerFixture
{
    [Fact]
    public void Should_WriteTheCorrectLogEntries_WhenTasksAreExecutedAndNotCancelled()
    {
        // Defines the task that needs to be executed.
        var task = new LongRunningServiceTaskImplementation();
        // Built a sequence in which logs should be created.
        var sequence = new MockSequence();
        LoggerMock.Setup(x => x.Information(It.IsAny<string>(), It.IsAny<string>())).Verifiable();
        LoggerMock.InSequence(sequence).Setup(x => x.Information("émqsdlfk", "smdlfksdmlfk")).Verifiable();
        LoggerMock.InSequence(sequence).Setup(x => x.Information(LoggingResources.LoggerTitle, LoggingResources.Logger_ServiceStopped)).Verifiable();
        LoggerMock.InSequence(sequence).Setup(x => x.Information(LoggingResources.LoggerTitle, LoggingResources.Logger_ServiceStarted)).Verifiable();
        LoggerMock.InSequence(sequence).Setup(x => x.Information(LoggingResources.LoggerTitle, string.Format(CultureInfo.InvariantCulture, LoggingResources.Logger_TaskCompleted, task.TaskName))).Verifiable();
        LoggerMock.InSequence(sequence).Setup(x => x.Information(LoggingResources.LoggerTitle, LoggingResources.Logger_ServiceStopped)).Verifiable();
        // Setup the mock required for the tests.
        TaskGathererMock.Setup(x => x.GetServiceTasks(LoggerMock.Object)).Returns(() =>
        {
            return new[] { task };
        });
        // Start the scheduler.
        Scheduler.Start(TaskGathererMock.Object, ConfigurationManagerMock.Object);
        // Wait for 5 seconds (this simulates running the service for 5 seconds).
        // Since our tasks execution time takes 4 seconds, all the assigned tasks should have been completed.
        Thread.Sleep(5000);
        // Stop the service. (We assume that all the tasks have been completed).
        Scheduler.Stop();
        LoggerMock.VerifyAll();
    }
}

所以,我的测试的第一步是设置日志,然后执行测试本身(这会导致对日志记录器的调用),最后我验证它。

然而,测试总是通过。

在这种情况下它应该失败,因为下面的调用:

LoggerMock.InSequence(sequence).Setup(x => x.Information("émqsdlfk", "smdlfksdmlfk")).Verifiable();

试图理解MockSequence

虽然这肯定感觉像是Moq中的一个bug(参见Patrick Quirk对该问题的第一条评论),但这里有一个你可以做些什么的粗略想法。这是一种"长评论"。

创建一个简单的类:

class SequenceTracker
{
    int? state;
    public void Next(int newState)
    {
        if (newState <= state)
            Assert.Fail("Bad ordering there! States should be increasing.");
        state = newState;
    }
}

然后像这样使用它(你自己的代码的修改版本):

public void Should_WriteTheCorrectLogEntries_WhenTasksAreExecutedAndNotCancelled()
{
    // Defines the task that needs to be executed.
    var task = new LongRunningServiceTaskImplementation();
    // USE THE CLASS I PROPOSE:
    var tracker = new SequenceTracker();

    //LoggerMock.Setup(x => x.Information(It.IsAny<string>(), It.IsAny<string>()))
    LoggerMock.Setup(x => x.Information("émqsdlfk", "smdlfksdmlfk"))
        .Callback(() => tracker.Next(10));
    LoggerMock.Setup(x => x.Information(LoggingResources.LoggerTitle, LoggingResources.Logger_ServiceStopped))
        .Callback(() => tracker.Next(20));
    LoggerMock.Setup(x => x.Information(LoggingResources.LoggerTitle, LoggingResources.Logger_ServiceStarted))
        .Callback(() => tracker.Next(30));
    LoggerMock.Setup(x => x.Information(LoggingResources.LoggerTitle, string.Format(CultureInfo.InvariantCulture, LoggingResources.Logger_TaskCompleted, task.TaskName)))
        .Callback(() => tracker.Next(40));
    LoggerMock.Setup(x => x.Information(LoggingResources.LoggerTitle, LoggingResources.Logger_ServiceStopped))
        .Callback(() => tracker.Next(50));
    // Setup the mock required for the tests.
    TaskGathererMock.Setup(x => x.GetServiceTasks(LoggerMock.Object)).Returns(() =>
    {
        return new[] { task };
    });
    // Start the scheduler.
    Scheduler.Start(TaskGathererMock.Object, ConfigurationManagerMock.Object);
    // Wait for 5 seconds (this simulates running the service for 5 seconds).
    // Since our tasks execution time takes 4 seconds, all the assigned tasks should have been completed.
    Thread.Sleep(5000);
    // Stop the service. (We assume that all the tasks have been completed).
    Scheduler.Stop();
    // THIS NOW WORKS BECAUSE WE ABANDONED THE 'MockSequence' APPROACH:
    LoggerMock.VerifyAll();
}

受Jeppe Stig Nielsen回答的启发,我延续了他的想法,我认为这在一定程度上解决了你的问题(我希望在Moq中完成)。这将验证模拟序列的完整执行:

public class SequenceVerifyer
{
    public MockSequence Sequence { get; private set; } = new MockSequence();
    public Action NextCallback()
    {
        var callNo = setupCount++;
        return () => { AssertCallNo(callNo);};
    }
    public void VerifyAll()
    {
        Assert.AreEqual(setupCount, executionCount, $"less calls ({executionCount}) executed than previously set up ({setupCount}).");
    }
    private int executionCount = 0;
    private int setupCount = 0;
    private void AssertCallNo(int expectedCallNo)
    {
        Assert.AreEqual(executionCount, expectedCallNo, $"order of call is wrong. this call is marked with No ({expectedCallNo}), but ({executionCount}) was expected.");
        executionCount++;
    }
}

用法:

public interface IFoo
{
    bool foo(int n);
}
public interface IBar
{
    int bar(string a);
}
[Test]
public void SequenceVerifyer_with_full_sequence()
{
    var fooMock = new Mock<IFoo>(MockBehavior.Strict);
    var barMock = new Mock<IBar>(MockBehavior.Strict);
    var seq = new SequenceVerifyer();
    fooMock.Setup(f => f.foo(3)).Returns(false);
    barMock.Setup(b => b.bar("world")).Returns(4);
    fooMock.InSequence(seq.Sequence).Setup(f => f.foo(4)).Returns(true).Callback(seq.NextCallback());
    barMock.InSequence(seq.Sequence).Setup(b => b.bar("hello")).Returns(2).Callback(seq.NextCallback());
    fooMock.InSequence(seq.Sequence).Setup(f => f.foo(4)).Returns(false).Callback(seq.NextCallback());
    fooMock.Object.foo(3); //non sequence
    fooMock.Object.foo(4);
    barMock.Object.bar("hello");
    barMock.Object.bar("world"); //non sequence
    fooMock.Object.foo(4);
    fooMock.VerifyAll();
    barMock.VerifyAll();
    seq.VerifyAll();
}

受到这个答案的启发

使用

            void Test()
            {
                // Arrange
                var fooMock = new Mock<IFoo>(MockBehavior.Strict);
                using var seq = SequenceVerifier.For(fooMock);
                seq.Setup(f => f.Foo(1), true);
                seq.Setup(f => f.Foo(2), false);
                // Act
                fooMock.Object.Foo(1);
                fooMock.Object.Foo(2);
            }
<标题> 实现
#nullable enable
using FluentAssertions;
using Moq;
using System;
using System.Linq.Expressions;
using System.Threading.Tasks;
namespace Tests.Core
{
    public abstract class SequenceVerifier : IDisposable
    {
        public static SequenceVerifier<T> For<T>(Mock<T> mock)
            where T : class
        {
            return new SequenceVerifier<T>(mock);
        }
        public void Dispose() => Dispose(true);
        protected virtual void Dispose(bool disposing) { }
    }
    public sealed class SequenceVerifier<T> : SequenceVerifier
        where T : class
    {
        private int expectedCalls;
        private int actualCalls;
        public readonly MockSequence Sequence = new MockSequence();
        public Mock<T> Mock { get; }
        public SequenceVerifier(Mock<T> mock)
        {
            Mock = mock ?? throw new ArgumentNullException(nameof(mock));
            if (Mock.Behavior != MockBehavior.Strict)
            {
                throw new ArgumentException($"{nameof(MockBehavior)} must be {nameof(MockBehavior.Strict)}");
            }
        }
        public Action NextCallback()
        {
            int currentCall = expectedCalls++;
            return () => AssertCallNo(currentCall);
        }
        public void VerifyAll()
        {
            actualCalls.Should().Be(expectedCalls, $"the number of actual calls must be the same as previously set up");
        }
        private void AssertCallNo(int expectedCallNo)
        {
            actualCalls.Should().Be(expectedCallNo, $"calls must be in order");
            actualCalls++;
        }
        public void Setup<TResult>(Expression<Func<T, TResult?>> expression, TResult? result = default)
        {
            if (typeof(TResult) == typeof(Task) && result is null)
            {
                result = (TResult)(object)Task.CompletedTask;
            }
            Mock.InSequence(Sequence)
                .Setup(expression)
                .Returns(result)
                .Callback(NextCallback());
        }
        protected override void Dispose(bool disposing) 
        {
            if (disposing)
            {
                Mock.VerifyAll();
                VerifyAll();
            }
        }
    }
}
<标题> 单元测试
#nullable enable
using static FluentAssertions.FluentActions;
using Moq;
using Xunit;
using FluentAssertions;
using System;
namespace Tests.Core
{
    public class SequenceVerifierFixture
    {
        public interface IFoo
        {
            bool Foo(int n);
        }
        [Fact]
        public void Given2SetupsAndNonSequenceSetup_When2CallsInOrderAndNonSequenceCall_ThenVerificationSuccessful()
        {
            var fooMock = new Mock<IFoo>(MockBehavior.Strict);
            var seq = SequenceVerifier.For(fooMock);
            fooMock.Setup(f => f.Foo(3)).Returns(false);
            seq.Setup(f => f.Foo(4), true);
            seq.Setup(f => f.Foo(4), false);
            fooMock.Object.Foo(3); //non sequence
            fooMock.Object.Foo(4);
            fooMock.Object.Foo(4);
            fooMock.VerifyAll();
            seq.VerifyAll();
        }
        [Fact]
        public void Given2Setups_WhenCallOnly1stSetupThenDispose_ThenVerificationFails()
        {
            // Arrange
            var fooMock = new Mock<IFoo>(MockBehavior.Strict);
            var seq = SequenceVerifier.For(fooMock);
            seq.Setup(f => f.Foo(1), true);
            seq.Setup(f => f.Foo(2), false);
            // Act
            fooMock.Object.Foo(1);
            // Assert
            Invoking(seq.Dispose)
                .Should().Throw<Exception>();
        }
        [Fact]
        public void Given2Setups_WhenCallOnly1stSetupWithinUsing_ThenVerificationFails()
        {
            void WithinUsing()
            {
                // Arrange
                var fooMock = new Mock<IFoo>(MockBehavior.Strict);
                using var seq = SequenceVerifier.For(fooMock);
                seq.Setup(f => f.Foo(1), true);
                seq.Setup(f => f.Foo(2), false);
                // Act
                fooMock.Object.Foo(1);
            }
            // Assert
            Invoking(WithinUsing)
                .Should().Throw<Exception>();
        }
        [Fact]
        public void Given2Setups_WhenCall2SetupsThenExtraCall_ThenVerificationFails()
        {
            // Arrange
            var fooMock = new Mock<IFoo>(MockBehavior.Strict);
            var seq = SequenceVerifier.For(fooMock);
            seq.Setup(f => f.Foo(1), true);
            seq.Setup(f => f.Foo(2), false);
            // Act
            fooMock.Object.Foo(1);
            fooMock.Object.Foo(2);
            // Assert
            Invoking(() => fooMock.Object.Foo(1))
                .Should().Throw<Exception>();
        }
        [Fact]
        public void Given2Setups_WhenCallSetup1Twice_ThenVerificationFails()
        {
            // Arrange
            var fooMock = new Mock<IFoo>(MockBehavior.Strict);
            var seq = SequenceVerifier.For(fooMock);
            seq.Setup(f => f.Foo(1), true);
            seq.Setup(f => f.Foo(2), false);
            // Act
            fooMock.Object.Foo(1);
            // Assert
            Invoking(() => fooMock.Object.Foo(1))
                .Should().Throw<Exception>();
        }
    }
}
相关文章: