ReactiveUI 中的 TestScheduler 异步方法死锁
Async method deadlocks with TestScheduler in ReactiveUI
我正在尝试在测试中使用带有异步方法的 reactiveui 测试调度程序。
等待异步调用时测试挂起。
根本原因似乎是在异步方法中等待的命令。
[Fact]
public async Task Test()
=> await new TestScheduler().With(async scheduler =>
{
await SomeAsyncMethod();
// *** execution never gets here
Debugger.Break();
});
private async Task SomeAsyncMethod()
{
var command = ReactiveCommand.CreateFromTask(async () =>
{
await Task.Delay(100);
});
// *** this hangs
await command.Execute();
}
如何结合不会死锁的测试调度程序进行异步调用?
我正在使用 reactiveui 9.4.1
编辑:
我已经尝试了 Funks 回答中建议的 WithAsync()
方法,但行为是一样的。
How can I do an async call in combination with the test scheduler?
简而言之
command.Execute()
是冷可观察量。您需要订阅它,而不是使用 await
.
鉴于你对 TestScheduler
的兴趣,我想你想测试一些涉及时间的东西。但是,从 When should I care about scheduling 部分:
threads created via "new Thread()" or "Task.Run" can't be controlled in a unit test.
因此,如果您想检查,例如,您的 Task
是否在 100 毫秒内完成,您将不得不等到异步方法完成。可以肯定的是,不是那种测试TestScheduler
。
稍长的版本
TestScheduler
的目的是通过在特定时间点启动事物并验证状态来验证工作流程。由于我们只能在 TestScheduler
上操纵时间,您通常不希望等待真正的异步代码完成,因为无法快进实际计算或 I/O。请记住,这是关于验证工作流:vm.A
在 20 毫秒时有新值,所以 vm.B
应该在 120 毫秒时有新值,...
那么如何测试 SUT?
1\ 您可以使用 scheduler.CreateColdObservable
模拟异步方法
public class ViewModelTests
{
[Fact]
public void Test()
{
string observed = "";
new TestScheduler().With(scheduler =>
{
var observable = scheduler.CreateColdObservable(
scheduler.OnNextAt(100, "Done"));
observable.Subscribe(value => observed = value);
Assert.Equal("", observed);
scheduler.AdvanceByMs(99);
Assert.Equal("", observed);
scheduler.AdvanceByMs(1);
Assert.Equal("Done", observed);
});
}
}
这里我们基本上用 scheduler
上创建的 var observable
替换了 command.Execute()
。
很明显,上面的示例相当简单,但是通过多个可观察对象相互通知,这种测试可以提供有价值的见解,以及重构时的安全网。
参考:
- Answer by Paul Betts
- Control Time with the
TestScheduler
2\ 您可以显式引用 IScheduler
a) 使用 RxApp
提供的调度程序
public class MyViewModel : ReactiveObject
{
public string Observed { get; set; }
public MyViewModel()
{
Observed = "";
this.MyCommand = ReactiveCommand
.CreateFromTask(SomeAsyncMethod);
}
public ReactiveCommand<Unit, Unit> MyCommand { get; }
private async Task SomeAsyncMethod()
{
await RxApp.TaskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(100));
Observed = "Done";
}
}
public class ViewModelTests
{
[Fact]
public void Test()
{
new TestScheduler().With(scheduler =>
{
var vm = new MyViewModel();
vm.MyCommand.Execute().Subscribe();
Assert.Equal("", vm.Observed);
scheduler.AdvanceByMs(99);
Assert.Equal("", vm.Observed);
scheduler.AdvanceByMs(1);
Assert.Equal("Done", vm.Observed);
});
}
}
备注
CreateFromTask
创建一个具有异步执行逻辑的 ReactiveCommand
。无需将 Test
方法定义为异步或等待 TestScheduler
.
在 With
扩展方法的范围内 RxApp.TaskpoolScheduler
= RxApp.MainThreadScheduler
= new TestScheduler()
.
b) 通过构造函数注入管理你自己的调度器
public class MyViewModel : ReactiveObject
{
private readonly IScheduler _taskpoolScheduler;
public string Observed { get; set; }
public MyViewModel(IScheduler scheduler)
{
_taskpoolScheduler = scheduler;
Observed = "";
this.MyCommand = ReactiveCommand
.CreateFromTask(SomeAsyncMethod);
}
public ReactiveCommand<Unit, Unit> MyCommand { get; }
private async Task SomeAsyncMethod()
{
await _taskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(100));
Observed = "Done";
}
}
public class ViewModelTests
{
[Fact]
public void Test()
{
new TestScheduler().With(scheduler =>
{
var vm = new MyViewModel(scheduler); ;
vm.MyCommand.Execute().Subscribe();
Assert.Equal("", vm.Observed);
scheduler.AdvanceByMs(99);
Assert.Equal("", vm.Observed);
scheduler.AdvanceByMs(0);
Assert.Equal("Done", vm.Observed);
});
}
}
参考:
让我们用来自 Haacked 的另一句话来拉近距离:
Unfortunately, and this next point is important, the TestScheduler
doesn’t extend into real life, so your shenanigans are limited to your asynchronous Reactive code. Thus, if you call Thread.Sleep(1000)
in your test, that thread will really be blocked for a second. But as far as the test scheduler is concerned, no time has passed.
调用嵌套方法时是否尝试使用ConfigureAwait(false)?
[Fact]
public async Task Test()
=> await new TestScheduler().With(async scheduler =>
{
// this hangs
await SomeAsyncMethod().ConfigureAwait(false);
// ***** execution will never get to here
Debugger.Break();
}
请尝试在所有异步方法上使用 .ConfigureAwait(false)
。
这将为您提供非阻塞行为。
[Fact]
public async Task Test()
=> await new TestScheduler().With(async scheduler =>
{
await SomeAsyncMethod().ConfigureAwait(false);
// *** execution never gets here
Debugger.Break();
}).ConfigureAwait(false);
private async Task SomeAsyncMethod()
{
var command = ReactiveCommand.CreateFromTask(async () =>
{
await Task.Delay(100).ConfigureAwait(false);
}).ConfigureAwait(false);
// *** this hangs
await command.Execute();
}
另一种测试问题是否与 ConfigureAwait 相关的方法是将您的项目移植到 Asp.Net Core 并在那里进行测试。
Asp.net核心不需要使用ConfigureAwait来防止这个阻塞问题。
我正在尝试在测试中使用带有异步方法的 reactiveui 测试调度程序。
等待异步调用时测试挂起。
根本原因似乎是在异步方法中等待的命令。
[Fact]
public async Task Test()
=> await new TestScheduler().With(async scheduler =>
{
await SomeAsyncMethod();
// *** execution never gets here
Debugger.Break();
});
private async Task SomeAsyncMethod()
{
var command = ReactiveCommand.CreateFromTask(async () =>
{
await Task.Delay(100);
});
// *** this hangs
await command.Execute();
}
如何结合不会死锁的测试调度程序进行异步调用?
我正在使用 reactiveui 9.4.1
编辑:
我已经尝试了 Funks 回答中建议的 WithAsync()
方法,但行为是一样的。
How can I do an async call in combination with the test scheduler?
简而言之
command.Execute()
是冷可观察量。您需要订阅它,而不是使用 await
.
鉴于你对 TestScheduler
的兴趣,我想你想测试一些涉及时间的东西。但是,从 When should I care about scheduling 部分:
threads created via "new Thread()" or "Task.Run" can't be controlled in a unit test.
因此,如果您想检查,例如,您的 Task
是否在 100 毫秒内完成,您将不得不等到异步方法完成。可以肯定的是,不是那种测试TestScheduler
。
稍长的版本
TestScheduler
的目的是通过在特定时间点启动事物并验证状态来验证工作流程。由于我们只能在 TestScheduler
上操纵时间,您通常不希望等待真正的异步代码完成,因为无法快进实际计算或 I/O。请记住,这是关于验证工作流:vm.A
在 20 毫秒时有新值,所以 vm.B
应该在 120 毫秒时有新值,...
那么如何测试 SUT?
1\ 您可以使用 scheduler.CreateColdObservable
public class ViewModelTests
{
[Fact]
public void Test()
{
string observed = "";
new TestScheduler().With(scheduler =>
{
var observable = scheduler.CreateColdObservable(
scheduler.OnNextAt(100, "Done"));
observable.Subscribe(value => observed = value);
Assert.Equal("", observed);
scheduler.AdvanceByMs(99);
Assert.Equal("", observed);
scheduler.AdvanceByMs(1);
Assert.Equal("Done", observed);
});
}
}
这里我们基本上用 scheduler
上创建的 var observable
替换了 command.Execute()
。
很明显,上面的示例相当简单,但是通过多个可观察对象相互通知,这种测试可以提供有价值的见解,以及重构时的安全网。
参考:
- Answer by Paul Betts
- Control Time with the
TestScheduler
2\ 您可以显式引用 IScheduler
a) 使用 RxApp
提供的调度程序public class MyViewModel : ReactiveObject
{
public string Observed { get; set; }
public MyViewModel()
{
Observed = "";
this.MyCommand = ReactiveCommand
.CreateFromTask(SomeAsyncMethod);
}
public ReactiveCommand<Unit, Unit> MyCommand { get; }
private async Task SomeAsyncMethod()
{
await RxApp.TaskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(100));
Observed = "Done";
}
}
public class ViewModelTests
{
[Fact]
public void Test()
{
new TestScheduler().With(scheduler =>
{
var vm = new MyViewModel();
vm.MyCommand.Execute().Subscribe();
Assert.Equal("", vm.Observed);
scheduler.AdvanceByMs(99);
Assert.Equal("", vm.Observed);
scheduler.AdvanceByMs(1);
Assert.Equal("Done", vm.Observed);
});
}
}
备注
CreateFromTask
创建一个具有异步执行逻辑的ReactiveCommand
。无需将Test
方法定义为异步或等待TestScheduler
.在
With
扩展方法的范围内RxApp.TaskpoolScheduler
=RxApp.MainThreadScheduler
=new TestScheduler()
.
b) 通过构造函数注入管理你自己的调度器
public class MyViewModel : ReactiveObject
{
private readonly IScheduler _taskpoolScheduler;
public string Observed { get; set; }
public MyViewModel(IScheduler scheduler)
{
_taskpoolScheduler = scheduler;
Observed = "";
this.MyCommand = ReactiveCommand
.CreateFromTask(SomeAsyncMethod);
}
public ReactiveCommand<Unit, Unit> MyCommand { get; }
private async Task SomeAsyncMethod()
{
await _taskpoolScheduler.Sleep(TimeSpan.FromMilliseconds(100));
Observed = "Done";
}
}
public class ViewModelTests
{
[Fact]
public void Test()
{
new TestScheduler().With(scheduler =>
{
var vm = new MyViewModel(scheduler); ;
vm.MyCommand.Execute().Subscribe();
Assert.Equal("", vm.Observed);
scheduler.AdvanceByMs(99);
Assert.Equal("", vm.Observed);
scheduler.AdvanceByMs(0);
Assert.Equal("Done", vm.Observed);
});
}
}
参考:
让我们用来自 Haacked 的另一句话来拉近距离:
Unfortunately, and this next point is important, the
TestScheduler
doesn’t extend into real life, so your shenanigans are limited to your asynchronous Reactive code. Thus, if you callThread.Sleep(1000)
in your test, that thread will really be blocked for a second. But as far as the test scheduler is concerned, no time has passed.
调用嵌套方法时是否尝试使用ConfigureAwait(false)?
[Fact]
public async Task Test()
=> await new TestScheduler().With(async scheduler =>
{
// this hangs
await SomeAsyncMethod().ConfigureAwait(false);
// ***** execution will never get to here
Debugger.Break();
}
请尝试在所有异步方法上使用 .ConfigureAwait(false)
。
这将为您提供非阻塞行为。
[Fact]
public async Task Test()
=> await new TestScheduler().With(async scheduler =>
{
await SomeAsyncMethod().ConfigureAwait(false);
// *** execution never gets here
Debugger.Break();
}).ConfigureAwait(false);
private async Task SomeAsyncMethod()
{
var command = ReactiveCommand.CreateFromTask(async () =>
{
await Task.Delay(100).ConfigureAwait(false);
}).ConfigureAwait(false);
// *** this hangs
await command.Execute();
}
另一种测试问题是否与 ConfigureAwait 相关的方法是将您的项目移植到 Asp.Net Core 并在那里进行测试。
Asp.net核心不需要使用ConfigureAwait来防止这个阻塞问题。