这是我想出的一个解决方案。
我创造了自己的“Job”和“Ilock”,而不是嘲笑它们。
internal sealed class TestableJob : Job
{
private readonly ILock @lock;
public TestableJob(ILock @lock)
: base(@lock)
{
this.@lock = @lock;
}
public bool IsLockedBeforeJobExecution
{
get; set;
}
public override Task ExecuteAsync(IExecutionContext executionContext)
{
this.IsLockedBeforeJobExecution = this.@lock.IsLocked();
return Task.CompletedTask;
}
}
internal sealed class TestableLock : ILock
{
private bool isLockedFlag;
public bool IsLocked()
{
return this.isLockedFlag;
}
public void Lock()
{
this.isLockedFlag = true;
}
public void Unlock()
{
this.isLockedFlag = false;
}
}
对于某些测试,我使用这些自定义实现,而对于其他测试,我则使用mock。
[Theory(DisplayName = "The `Job` is NOT executed when the `ILock` is \"Locked\".")]
[AutoDomainData]
internal async Task TheJobIsNotExecutedWhenTheLockIsLocked([Frozen] Mock<ILock> lockMock,
[Frozen] Mock<Job> jobMock)
{
// VALIDATION.
ILock @lock = lockMock?.Object ?? throw new ArgumentNullException(nameof(lockMock));
Job job = jobMock?.Object ?? throw new ArgumentNullException(nameof(jobMock));
// MOCK SETUP.
_ = lockMock.Setup(x => x.IsLocked())
.Returns(true);
// ACT.
await job.StartAsync(new CancellationToken())
.ConfigureAwait(false);
// ASSERT.
jobMock.Verify(job => job.ExecuteAsync(It.IsAny<IExecutionContext>()), Times.Never);
}
[Theory(DisplayName = "The `Job` is executed when the `ILock` is \"NOT Locked\".")]
[AutoDomainData]
internal async Task TheJobIsExecutedWhenTheLockIsNotLocked([Frozen] Mock<ILock> lockMock,
[Frozen] Mock<Job> jobMock)
{
// VALIDATION.
ILock @lock = lockMock?.Object ?? throw new ArgumentNullException(nameof(lockMock));
Job job = jobMock?.Object ?? throw new ArgumentNullException(nameof(jobMock));
// MOCK SETUP.
_ = lockMock.Setup(x => x.IsLocked())
.Returns(false);
// ACT.
await job.StartAsync(new CancellationToken())
.ConfigureAwait(false);
// ASSERT.
jobMock.Verify(job => job.ExecuteAsync(It.IsAny<IExecutionContext>()), Times.Once);
}
[Fact(DisplayName = "The `ILock` is \"Locked\" before the `Job` is executed.")]
internal async Task TheLockIsLockedBeforeTheJobIsExecuted()
{
// ARRANGE.
var @lock = new TestableLock();
var job = new TestableJob(@lock);
// ACT.
await job.StartAsync(new CancellationToken())
.ConfigureAwait(false);
// ASSERT.
_ = job.IsLockedBeforeJobExecution
.Should()
.BeTrue();
}
[Fact(DisplayName = "The `ILock` is \"Unlocked\" once the `Job` is executed.")]
internal async Task TheLockIsUnlockedOnceTheJobIsExecuted()
{
// ARRANGE.
var @lock = new TestableLock();
var job = new TestableJob(@lock);
// ACT.
await job.StartAsync(new CancellationToken())
.ConfigureAwait(false);
// ASSERT.
_ = @lock.IsLocked()
.Should()
.BeFalse();
}
[Theory(DisplayName = "The `ILock` is \"Unlocked\" when the `Job` is stopped.")]
[AutoDomainData]
internal async Task TheLockIsUnlockedWhenTheJobIsStopped([Frozen] Mock<ILock> lockMock, [Frozen] Mock<Job> jobMock)
{
// VALIDATION.
ILock @lock = lockMock?.Object ?? throw new ArgumentNullException(nameof(lockMock));
Job job = jobMock?.Object ?? throw new ArgumentNullException(nameof(jobMock));
// ACT.
await job.StopAsync(new CancellationToken())
.ConfigureAwait(false);
// ASSERT.
lockMock.Verify(@lock => @lock.Unlock(), Times.Once);
}
[Theory(DisplayName = "The `ILock` is \"Unlocked\" when an exception is raised during the execution of the `Job`.")]
[AutoDomainData]
internal async Task TheLockIsUnlockedWhenAnExceptionIsRaisedDuringTheExecutionOfTheJob(
[Frozen] Mock<ILock> lockMock,
[Frozen] Mock<Job> jobMock)
{
// VALIDATION.
ILock @lock = lockMock?.Object ?? throw new ArgumentNullException(nameof(lockMock));
Job job = jobMock?.Object ?? throw new ArgumentNullException(nameof(jobMock));
// MOCK SETUP.
_ = jobMock.Setup(@mock => mock.ExecuteAsync(It.IsAny<IExecutionContext>()))
.Throws<ArgumentOutOfRangeException>();
// ACT.
await job.StartAsync(new CancellationToken())
.ConfigureAwait(false);
// ASSERT.
lockMock.Verify(@lock => @lock.Unlock(), Times.Once);
}