foreach循环中的单元测试中断

时间:2016-02-08 22:19:05

标签: c# multithreading unit-testing

我试图在类概念上类似于以下内容对取消执行方案进行单元测试:

public class ContextExecutor
{
    public ContextExecutor(IContextRunner runner, IExecutionCanceler canceler)
    {
        this.runner = runner;
        this.canceler = canceler;
    }

    public void Execute(IEnumerable<IContext> contexts)
    {
        foreach (var ctx in contexts)
        {
            if (canceler.IsCanceled)
            {
                break;
            }

            runner.Run(ctx);
        }
    }

    readonly IContextRunner runner;
    readonly IExecutionCanceler canceler;
}

public interface IContextRunner 
{
    void Run(IContext context);
}

public interface IExecutionCanceler 
{
    bool IsCanceled { get; }
}

我所经历的测试案例应该经过以下步骤:

  1. 以某种方式异步启动ContextExecutor.Execute();
  2. 暂停执行该方法,直到某些内容从单元测试代码解锁;
  3. 解锁执行并让它执行1(...或2)循环运行,无论如何不到完整的可枚举长度;
  4. 通过设置canceler.IsCanceled = true;
  5. 来调用取消
  6. 解锁循环执行;
  7. 同步等待方法完成;
  8. 声明循环已被调用预期的nr次。
  9. 我纠结于从单元测试代码控制循环执行锁定/解锁。除了在新线程中启动Execute()之外,我避免使用线程同步原语(例如信号量和锁)。我还必须放弃基于Task的方法,因为我无法更改签名以应用async/await构造。我尝试使用以下内容,但没有运气:

    • 将一个yield驱动的函数注入IEnumerable<IContext>输入参数以保持foreach()行的循环,每当另一个yield被命中时释放循环,并尝试从单元测试代码中控制它。

    • 注入一个由活动扩展IContextRunner runner提供支持的Subject来保持runner.Run行的循环,每当另一个Subject.OnNext被击中时释放循环,并尝试从单元测试代码中控制它。

    就此而言,单元测试框架是NUnit,而NSubstitute是模拟框架,FluentAssertion是首选的断言库。我知道如何安排/行动/断言。

    我错过了什么?感谢

    修改

    为了提供一个已经尝试过的例子,这是一个基于Task的方法,在发布问题和阅读@Peter Duniho后发表了有用的评论:

    // in unit test class
    
    ContextExecutor executor;
    IContextRunner runner;
    IExecutionCanceler canceler;
    IRunnableContext[] runnableContexts;
    int totalNrOfContexts;
    int nrOfContextToRun;  // this will be < totalNrOfContexts
    int actualNrOfContextRan;
    
    [SetUp]
    public virtual void before_each()
    {
        // create instance under test, mock dependencies, dummy input data
        Initialize();
    
        RunScenarioAsync().Wait();
    }
    
    async Task RunScenarioAsync()
    {
        // prepare mock IContextRunner so that for each input context:
        //  * there's a related TaskCompletionSource<Object> 
        //  * Run() increments actualNrOfContextRan
        //  * Run() performs taskSource.Task.Wait();
        List<TaskCompletionSource<Object>> runTaskSources = PrepareMockContextRunner();
    
        canceler.IsCanceled.Returns(false); // let execution go initially
    
        // queue up method under test to be processed asynchronously
        var executeTask = Task.Run(() =>
        {
            executor.Execute(runnableContexts);
        };
    
        // "unlock" some IContextRunner.Run() invocations,
        // for when they will be invoked
        for (int i = 0; i < nrOfContextToRun; i++)
        {
            runTaskSources[i].SetResult(null);
    
            await Task.Delay(0);  // tried also with Delay(1) and without this line at all
        }
    
        // flag to cancel execution
        canceler.IsCanceled.Returns(true);
    
        // unlock all remaining IContextRunner.Run() invocations,
        // again for when/if they will be invoked
        for (int i = nrOfContextToRun; i < totalNrOfContexts; i++)
        {
            runTaskSources[i].SetResult(null);
    
            await Task.Delay(0);
        }
    
        // wait until method under test completes
        await executeTask;
    }
    
    [Test]
    public void it_should_only_run_until_cancel()
    {
        int expected = nrOfContextToRun;
        int actual = actualNrOfContextRan;
    
        actual.Should().Be(expected);
    }
    

    我在这里遇到的问题(与其他尝试的方法类似)是以可预测的方式(即同步)给予和重新控制被测方法。 在这里,如果没有await Task.Delay()或延迟是0ms,实际上只运行了1个上下文:被测方法没有机会运行第二个和第三个,它很快就会找到取消标志。如果延迟是1ms,则在实际检测到标志之前,方法执行比预期更多的上下文。也尝试使用刻度而不是ms,但根据我的经验,玩延迟通常意味着你做错了。

0 个答案:

没有答案
相关问题