C# 触发属性更改的ReactiveList上的单元测试项已更改
归结起来:我需要延迟单元测试线程中的执行,以便可观察对象有时间更新属性。有没有一种无需求助于线程的反应式方法?睡眠 我有一个ViewModel,它管理一个“事物”的反应列表。ThingViewModel和主ViewModel都是从ReactiveObject派生的。Thing有一个IsSelected属性,ViewModel有一个SelectedThing属性,我根据观察IsSelected上的更改来保持同步。我有几个使用ViewModel的不同视图,这允许我在这些视图之间很好地同步所选内容。它是有效的。当我尝试对这种交互进行单元测试时,问题就出现了 ViewModel的构造函数中包含此订阅:C# 触发属性更改的ReactiveList上的单元测试项已更改,c#,unit-testing,reactiveui,C#,Unit Testing,Reactiveui,归结起来:我需要延迟单元测试线程中的执行,以便可观察对象有时间更新属性。有没有一种无需求助于线程的反应式方法?睡眠 我有一个ViewModel,它管理一个“事物”的反应列表。ThingViewModel和主ViewModel都是从ReactiveObject派生的。Thing有一个IsSelected属性,ViewModel有一个SelectedThing属性,我根据观察IsSelected上的更改来保持同步。我有几个使用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没有帮助,通过调用上的.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 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;
}
这里有几种不同的解决方案。首先,为了完整起见,让我们创建一个带有支持模型的简单的小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);
}
}
如果目标是INPC派生的,则插入/订阅属性已更改的事件,并设置一个可断言的标志。ReactiveObject支持INPC,但由于我在Reactive Land中玩,因此事件处理程序中的断言将不太理想。如果我想走这条路,我可以订阅属性changed observable并在那里断言。我更愿意像上面那样对实际属性进行断言,但我可以使用它作为回退…这在这一实例中可能就足够了。但是,线程也是如此。睡眠解决方案。这个问题的原因是,我想知道是否有一种方法可以做到这一点,即“反应型本地”。如果有,那么我将学习一些新的和有用的关于反应式的知识,这些知识将应用于当前之外的领域。让我们来看看。实际上,我可能应该将集合设置为私有,因为我只需要设置属性的ViewModel。不过,为了创建覆盖/模拟而对其进行保护比我所期望的更痛苦。我更喜欢基于Rx的解决方案。使用事件来检测基于Rx的更改的完成感觉有点像倒退。它确实有一个优点,我相信它确实会起作用,不过…@JacobProffitt我只是把它作为一个潜在的替代方案,如果没有其他解决方案出现的话。我还要提到的是,如果这主要是作为单元测试中的一个实用工具使用的,那么它不应该真正影响核心代码库的设计方式。我已经对它进行了测试,它确实起了作用。这是一个深思熟虑的答案,并且完成了任务。我希望能有一个更以Rx为中心的治疗方案,但在这一点上,这只是一个小小的、贫乏的希望……:)星期一我才能真正尝试一下,但它看起来很有趣!这需要一些欺骗(将我的ViewModel切换为使用RxApp.TaskpoolScheduler,并确保ViewModel实例化发生在“使用”中),但一旦我确定了参数,这就如预期的那样起作用了。做得好。
public async Task Test() {
//...
var listener = vm.OnPropertyChanged(_ => _.SelectedThing);
thingVM.IsSelected = true;
var actual = await listener;
Assert.AreEqual(actual, thingVM);
}
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);
}
}
[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);
}
}