我试图在类概念上类似于以下内容对取消执行方案进行单元测试:
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; }
}
我所经历的测试案例应该经过以下步骤:
ContextExecutor.Execute()
; canceler.IsCanceled = true
; 我纠结于从单元测试代码控制循环执行锁定/解锁。除了在新线程中启动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,但根据我的经验,玩延迟通常意味着你做错了。