代码之家  ›  专栏  ›  技术社区  ›  Complexity

通过观察外部行为对简单函数进行单元测试

  •  0
  • Complexity  · 技术社区  · 2 年前

    我正在编写一个Job类,为了确保这个作业只能执行一次,我引入了一个自定义的“锁定”机制。

    函数如下所示:

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        if (this.@lock.IsLocked())
        {
            return;
        }
    
        this.@lock.Lock();
    
        await this.ExecuteAsync(new JobExecutionContext(cancellationToken))
            .ConfigureAwait(false);
    
        this.@lock.Unlock();
    }
    

    现在,当我编写测试时,我应该测试外部可观察的行为,而不是测试实现细节,所以我现在有以下测试:

    [Theory(DisplayName = "Starting a `Job` (when the lock is locked), does NOT execute it.")]
    [AutoDomainData]
    public async Task StartingWithLockedLockDoesLockNotExecuteIt([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 = "Starting a `Job` (when the lock is NOT locked), does lock the lock.")]
    [AutoDomainData]
    public async Task StartingWithNotLockedLockDoesExecuteIt([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);
    }
    

    注意:我正在使用 AutoFixture ,但我遗漏了样板代码。

    现在,我有以下案例:

    • 锁定时,不会执行作业。
    • 如果未锁定,则执行作业。

    但我遗漏了以下重要案例:

    • 保证在执行期间,锁处于活动状态。

    如何正确测试? 我觉得设计应该更新,但我不知道怎么更新。

    有什么建议吗?

    0 回复  |  直到 2 年前
        1
  •  0
  •   PandaMagnus    2 年前

    你所说的“执行一次”是指永远执行一次,之后的每次调用都会返回相同的结果吗?还是说它一次只能执行一个作业?根据代码,我 假定 后者,但想检查一下是否清晰。

    以下是一些来自比我聪明得多的人的关于锁定的好信息: https://stackoverflow.com/a/12316461/10243808

    他的解释和提供的链接中的一些示例将为您提供许多处理返回内容和时间的选项,这些选项可能用于对最后一个场景进行单元测试(我想即使不进行重构,您也可以在if(@lock.isLocked())语句中返回一些错误消息。)尽管在重构之后,我认为您正在测试C#功能,我认为这是正确的 巨大的 大部分时间。

        2
  •  0
  •   Complexity    2 年前

    这是我想出的一个解决方案。

    我创造了自己的“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);
    }