体面的可测试异步操作模式

本文关键字:异步操作 模式 测试 体面 | 更新日期: 2023-09-27 18:13:40

我很难找到一个简单,灵活的模式,允许我在我的ViewModels中编写代码,在运行时将异步运行,但在测试时同步运行。这是我想出来的,有人有什么建议吗?这是一条好的道路吗?是否存在更好的现有模式?

LongRunningCall定义:

public class LongRunningCall
{
    public Action ExecuteAction { get; set; }
    public Action PostExecuteAction { get; set; }
    public LongRunningCall(Action executeAction = null, Action postExecuteAction = null)
    {
        ExecuteAction = executeAction;
        PostExecuteAction = postExecuteAction;
    }
    public void Execute(Action<Exception> onError)
    {
        try
        {
            ExecuteAction();
            PostExecuteAction();
        }
        catch (Exception ex)
        {
            if (onError == null)
                throw;
            onError(ex);
        }
    }
    public void ExecuteAsync(TaskScheduler scheduler, Action<Exception> onError)
    {
        var executeTask = Task.Factory.StartNew(ExecuteAction);
        var postExecuteTask = executeTask.ContinueWith((t) =>
            {
                if (t.Exception != null)
                    throw t.Exception;
                PostExecuteAction();
            }, scheduler);
        if (onError != null)
            postExecuteTask.ContinueWith((t) => { onError(t.Exception); });
    }
}

用法:

var continueCall = new LongRunningCall(continueCommand_Execute, continueCommand_PostExecute);
if (svc.IsAsyncRequired)
   continueCall.ExecuteAsync(TaskScheduler.FromCurrentSynchronizationContext(), continueCommand_Error);
else
   continueCall.Execute(continueCommand_Error);

唯一真正的先决条件是你需要在运行时知道你是否应该使用async/sync。当我运行单元测试时,我发送一个模拟,告诉我的代码同步运行,当应用程序实际运行时,isasyncrerequired默认为true;

反馈?

体面的可测试异步操作模式

我更愿意将是否同步或异步执行代码的决定封装在一个单独的类中,该类可以在接口后面抽象,例如:

public interface ITaskExecuter
{
    void ScheduleTask(
        Action executeAction,
        Action postExecuteAction,
        Action<Exception> onException);
}

可以在需要的地方注入实现ITaskExecuter的类的实例。您可以为测试和生产场景注入不同的实例。

用法就变成:

taskExecuter.ScheduleTask(
    continueCommand_Execute,
    continueCommand_PostExecute,
    continueCommand_Error);

在调用类中没有单独的代码路径,用于测试和生产。

您可以选择编写以下测试:

  • 检查传递给任务执行器的操作是否正确,或者
  • 配置任务执行器以同步执行操作测试所需的结果,或

我在目前的工作中做了一些非常类似的事情,但现在无法获得代码复制/粘贴…

基本上我所做的是创建一个IWorker接口,用DoWork(Func<>)方法。

然后我创建了2个派生类,一个'AsyncWorker'和一个'SyncWorker'。SyncWorker只是执行在Func传递(同步),而'AsyncWorker'是一个围绕BackgroundWorker的包装器,它将Func传递给BackgroundWorker进行异步处理。

然后,我改变了我的ViewModel有一个IWorker传入。这就把依赖解析移出了ViewModel,这样你就可以使用Dep. Inj。

由于我使用Unity,在我的单元测试配置中,我将IWorker映射到SyncWorker,并在生产中将IWorker映射到AsyncWorker

希望这是有意义的…我知道如果我手头有代码会更容易…

考虑更改ExecuteAsync,使其返回Task:

public Task ExecuteAsync(TaskScheduler scheduler, Action<Exception> onError)

所以在产品代码中,我就这样称呼它:

longRunningCall.ExecuteAsync(
    TaskScheduler.FromCurrentSynchronizationContext(),
    continueCommand_Error);

但是在单元测试中,我会等待任务实际完成:

var task = longRunningCall.ExecuteAsync(
    TaskScheduler.FromCurrentSynchronizationContext(),
    continueCommand_Error);
task.Wait();