试图理解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();
虽然这肯定感觉像是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>();
}
}
}
标题>标题>