ReactiveList 上的单元测试 ItemChanged 触发 属性 更改
Unit Testing ItemChanged on ReactiveList Triggering Property Change
归结为:我需要在我的单元测试线程中延迟执行,以便 observable 有时间更新 属性。有没有一种反应式的方法可以做到这一点而不必诉诸 Thread.Sleep?
我有一个 ViewModel 管理 "Thing" 的 ReactiveList。 ThingViewModel 和主 ViewModel 都派生自 ReactiveObject。 Thing 有一个 IsSelected 属性,ViewModel 有一个 SelectedThing 属性,我根据观察 IsSelected 的变化保持同步。我有几个使用 ViewModel 的不同视图,这使我可以很好地同步这些视图之间的选定事物。它有效。当我尝试对该交互进行单元测试时出现问题。
ViewModel 在其构造函数中有此订阅:
Things.ItemChanged.Where(c => c.PropertyName.Equals(nameof(ThingViewModel.IsSelected))).ObserveOn(ThingScheduler).Subscribe(c =>
{
SelectedThing = Things.FirstOrDefault(thing => thing.IsSelected);
});
在我的单元测试中,这个断言总是失败:
thingVM.IsSelected = true;
Assert.AreEqual(vm.SelectedThing, thingVM);
但是这个断言总是通过:
thingVM.IsSelected = true;
Thread.Sleep(5000);
Assert.AreEqual(vm.SelectedThing, thingVM);
本质上,我需要等待足够长的时间让订阅完成更改。 (我真的不需要等那么久,因为 UI 中的 运行 非常快。)
我尝试在我的单元测试中添加一个观察者来等待该处理,希望它们是相似的。但这是一次洗礼。这是无效的。
var itemchanged = vm.Things.ItemChanged.Where(x => x.PropertyName.Equals("IsSelected")).AsObservable();
. . .
thingVM.IsSelected = true;
var changed = itemChanged.Next();
Assert.AreEqual(vm.SelectedThing, thingVM);
四处移动 itemChanged.Next 没有帮助,也没有通过在 changed
上调用 .First() 或 .Any() 来触发迭代(这两个都挂起进程,因为它阻止了从未发生过的通知的线程)。
所以。是否有一种响应式方法来等待该交互,以便我可以断言 属性 更改正在正确发生?我用 TestScheduler 弄乱了一些(无论如何我需要为 UI 手动设置调度程序,所以这并不难),但这似乎不适用,因为给予调度程序的实际操作发生在我的 ViewModel 中在 ItemChanged 上,我找不到以 TestScheduler 似乎设置工作的方式触发该触发器的方法。
尝试订阅 PropertyChanged
事件的简单想法,我创建了这个扩展方法
public static Task OnPropertyChanged<T>(this T target, string propertyName) where T : INotifyPropertyChanged {
var tcs = new TaskCompletionSource<object>();
PropertyChangedEventHandler handler = null;
handler = (sender, args) => {
if (string.Equals(args.PropertyName, propertyName, StringComparison.InvariantCultureIgnoreCase)) {
target.PropertyChanged -= handler;
tcs.SetResult(0);
}
};
target.PropertyChanged += handler;
return tcs.Task;
}
这将等待引发 属性 更改的事件,而不必阻塞线程。
例如,
public async Task Test() {
//...
var listener = vm.OnPropertyChanged("SelectedThing");
thingVM.IsSelected = true;
await listener;
Assert.AreEqual(vm.SelectedThing, thingVM);
}
然后我使用表达式进一步改进它以摆脱魔法字符串以及 return 正在监视的 属性 的值。
public static Task<TResult> OnPropertyChanged<T, TResult>(this T target, Expression<Func<T, TResult>> propertyExpression) where T : INotifyPropertyChanged {
var tcs = new TaskCompletionSource<TResult>();
PropertyChangedEventHandler handler = null;
var member = propertyExpression.GetMemberInfo();
var propertyName = member.Name;
if (member.MemberType != MemberTypes.Property)
throw new ArgumentException(string.Format("{0} is an invalid property expression", propertyName));
handler = (sender, args) => {
if (string.Equals(args.PropertyName, propertyName, StringComparison.InvariantCultureIgnoreCase)) {
target.PropertyChanged -= handler;
var value = propertyExpression.Compile()(target);
tcs.SetResult(value);
}
};
target.PropertyChanged += handler;
return tcs.Task;
}
/// <summary>
/// Converts an expression into a <see cref="System.Reflection.MemberInfo"/>.
/// </summary>
/// <param name="expression">The expression to convert.</param>
/// <returns>The member info.</returns>
public static MemberInfo GetMemberInfo(this Expression expression) {
var lambda = (LambdaExpression)expression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression) {
var unaryExpression = (UnaryExpression)lambda.Body;
memberExpression = (MemberExpression)unaryExpression.Operand;
} else
memberExpression = (MemberExpression)lambda.Body;
return memberExpression.Member;
}
并且喜欢使用
public async Task Test() {
//...
var listener = vm.OnPropertyChanged(_ => _.SelectedThing);
thingVM.IsSelected = true;
var actual = await listener;
Assert.AreEqual(actual, thingVM);
}
这里有几个不同的解决方案。首先,为了完成让我们创建一个带有支持模型的简单的小 ViewModel。
public class Foo : ReactiveObject
{
private Bar selectedItem;
public Bar SelectedItem
{
get => selectedItem;
set => this.RaiseAndSetIfChanged(ref selectedItem, value);
}
public ReactiveList<Bar> List { get; }
public Foo()
{
List = new ReactiveList<Bar>();
List.ChangeTrackingEnabled = true;
List.ItemChanged
.ObserveOn(RxApp.TaskpoolScheduler)
.Subscribe(_ => { SelectedItem = List.FirstOrDefault(x => x.IsSelected); });
}
}
public class Bar : ReactiveObject
{
private bool isSelected;
public bool IsSelected
{
get => isSelected;
set => this.RaiseAndSetIfChanged(ref isSelected, value);
}
}
一个 hacky 修复(我不推荐)是将 ObserveOn
更改为 SubscribeOn
。请在此处查看答案以获得更好的解释:
我给出的建议是将 reactiveui-testing 导入到您的单元测试中。那时你可以用 ImmediateScheduler
覆盖调度程序,这将强制所有内容立即安排在单个线程上
[TestMethod]
public void TestMethod1()
{
using (TestUtils.WithScheduler(ImmediateScheduler.Instance))
{
var bar = new Bar();
var foo = new Foo();
foo.List.Add(bar);
bar.IsSelected = true;
Assert.AreEqual(bar, foo.SelectedItem);
}
}
归结为:我需要在我的单元测试线程中延迟执行,以便 observable 有时间更新 属性。有没有一种反应式的方法可以做到这一点而不必诉诸 Thread.Sleep?
我有一个 ViewModel 管理 "Thing" 的 ReactiveList。 ThingViewModel 和主 ViewModel 都派生自 ReactiveObject。 Thing 有一个 IsSelected 属性,ViewModel 有一个 SelectedThing 属性,我根据观察 IsSelected 的变化保持同步。我有几个使用 ViewModel 的不同视图,这使我可以很好地同步这些视图之间的选定事物。它有效。当我尝试对该交互进行单元测试时出现问题。
ViewModel 在其构造函数中有此订阅:
Things.ItemChanged.Where(c => c.PropertyName.Equals(nameof(ThingViewModel.IsSelected))).ObserveOn(ThingScheduler).Subscribe(c =>
{
SelectedThing = Things.FirstOrDefault(thing => thing.IsSelected);
});
在我的单元测试中,这个断言总是失败:
thingVM.IsSelected = true;
Assert.AreEqual(vm.SelectedThing, thingVM);
但是这个断言总是通过:
thingVM.IsSelected = true;
Thread.Sleep(5000);
Assert.AreEqual(vm.SelectedThing, thingVM);
本质上,我需要等待足够长的时间让订阅完成更改。 (我真的不需要等那么久,因为 UI 中的 运行 非常快。)
我尝试在我的单元测试中添加一个观察者来等待该处理,希望它们是相似的。但这是一次洗礼。这是无效的。
var itemchanged = vm.Things.ItemChanged.Where(x => x.PropertyName.Equals("IsSelected")).AsObservable();
. . .
thingVM.IsSelected = true;
var changed = itemChanged.Next();
Assert.AreEqual(vm.SelectedThing, thingVM);
四处移动 itemChanged.Next 没有帮助,也没有通过在 changed
上调用 .First() 或 .Any() 来触发迭代(这两个都挂起进程,因为它阻止了从未发生过的通知的线程)。
所以。是否有一种响应式方法来等待该交互,以便我可以断言 属性 更改正在正确发生?我用 TestScheduler 弄乱了一些(无论如何我需要为 UI 手动设置调度程序,所以这并不难),但这似乎不适用,因为给予调度程序的实际操作发生在我的 ViewModel 中在 ItemChanged 上,我找不到以 TestScheduler 似乎设置工作的方式触发该触发器的方法。
尝试订阅 PropertyChanged
事件的简单想法,我创建了这个扩展方法
public static Task OnPropertyChanged<T>(this T target, string propertyName) where T : INotifyPropertyChanged {
var tcs = new TaskCompletionSource<object>();
PropertyChangedEventHandler handler = null;
handler = (sender, args) => {
if (string.Equals(args.PropertyName, propertyName, StringComparison.InvariantCultureIgnoreCase)) {
target.PropertyChanged -= handler;
tcs.SetResult(0);
}
};
target.PropertyChanged += handler;
return tcs.Task;
}
这将等待引发 属性 更改的事件,而不必阻塞线程。
例如,
public async Task Test() {
//...
var listener = vm.OnPropertyChanged("SelectedThing");
thingVM.IsSelected = true;
await listener;
Assert.AreEqual(vm.SelectedThing, thingVM);
}
然后我使用表达式进一步改进它以摆脱魔法字符串以及 return 正在监视的 属性 的值。
public static Task<TResult> OnPropertyChanged<T, TResult>(this T target, Expression<Func<T, TResult>> propertyExpression) where T : INotifyPropertyChanged {
var tcs = new TaskCompletionSource<TResult>();
PropertyChangedEventHandler handler = null;
var member = propertyExpression.GetMemberInfo();
var propertyName = member.Name;
if (member.MemberType != MemberTypes.Property)
throw new ArgumentException(string.Format("{0} is an invalid property expression", propertyName));
handler = (sender, args) => {
if (string.Equals(args.PropertyName, propertyName, StringComparison.InvariantCultureIgnoreCase)) {
target.PropertyChanged -= handler;
var value = propertyExpression.Compile()(target);
tcs.SetResult(value);
}
};
target.PropertyChanged += handler;
return tcs.Task;
}
/// <summary>
/// Converts an expression into a <see cref="System.Reflection.MemberInfo"/>.
/// </summary>
/// <param name="expression">The expression to convert.</param>
/// <returns>The member info.</returns>
public static MemberInfo GetMemberInfo(this Expression expression) {
var lambda = (LambdaExpression)expression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression) {
var unaryExpression = (UnaryExpression)lambda.Body;
memberExpression = (MemberExpression)unaryExpression.Operand;
} else
memberExpression = (MemberExpression)lambda.Body;
return memberExpression.Member;
}
并且喜欢使用
public async Task Test() {
//...
var listener = vm.OnPropertyChanged(_ => _.SelectedThing);
thingVM.IsSelected = true;
var actual = await listener;
Assert.AreEqual(actual, thingVM);
}
这里有几个不同的解决方案。首先,为了完成让我们创建一个带有支持模型的简单的小 ViewModel。
public class Foo : ReactiveObject
{
private Bar selectedItem;
public Bar SelectedItem
{
get => selectedItem;
set => this.RaiseAndSetIfChanged(ref selectedItem, value);
}
public ReactiveList<Bar> List { get; }
public Foo()
{
List = new ReactiveList<Bar>();
List.ChangeTrackingEnabled = true;
List.ItemChanged
.ObserveOn(RxApp.TaskpoolScheduler)
.Subscribe(_ => { SelectedItem = List.FirstOrDefault(x => x.IsSelected); });
}
}
public class Bar : ReactiveObject
{
private bool isSelected;
public bool IsSelected
{
get => isSelected;
set => this.RaiseAndSetIfChanged(ref isSelected, value);
}
}
一个 hacky 修复(我不推荐)是将 ObserveOn
更改为 SubscribeOn
。请在此处查看答案以获得更好的解释:
我给出的建议是将 reactiveui-testing 导入到您的单元测试中。那时你可以用 ImmediateScheduler
覆盖调度程序,这将强制所有内容立即安排在单个线程上
[TestMethod]
public void TestMethod1()
{
using (TestUtils.WithScheduler(ImmediateScheduler.Instance))
{
var bar = new Bar();
var foo = new Foo();
foo.List.Add(bar);
bar.IsSelected = true;
Assert.AreEqual(bar, foo.SelectedItem);
}
}