所以我在Xamarin移动应用程序中为我的一个视图模型编写单元测试。我测试的方法如下:
public async Task RefreshItems()
{
var departamentsObservable = _dataService.GetDepartaments();
departamentsObservable.ObserveOn(SynchronizationContext.Current).Subscribe(items =>
{
Departaments.ReplaceWithRange(items);
});
await departamentsObservable.FirstOrDefaultAsync();
}
_dataService.GetDepartaments();
方法返回IObservable<IEnumerable<Departament>>
。
我使用Observable
和Subscribe
代替返回Task<IEnumerable<Departament>>
的简单方法,因为在我的情况下Observable
将&#34;返回&#34;两次(一次是来自缓存的数据,另一次是新获取的数据,来自web)。
对于测试我当然是模拟_dataService.GetDepartaments();
方法:
public IObservable<IEnumerable<Departament>> GetDepartaments()
{
return Observable.Return(MockData.Departaments);
}
因此方法立即返回模拟数据。
我对RefreshItems
方法的测试看起来像这样:
[Fact]
public async Task RefreshItemsTest()
{
await _viewModel.RefreshItems();
Assert.Equal(MockData.Departaments, _viewModel.Departaments,
new DepartamentComparer());
}
问题是该测试随机失败(大约10次中有1次)。基本上视图模型中的Departaments
集合应在Observable
&#34;返回&#34;时更新。是空的。
我应该补充一点,我在Xamarin Studio中使用xUnit 2.1.0测试框架和xUnit控制台。
修改 只有在测试运行器中运行时,Enigmativity的建议才会抛出 Sequence不包含任何元素异常。下面是用于演示该问题的最小woking示例代码:
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Reactive.Linq;
using System.Threading;
using System.Collections.Generic;
namespace TestApp
{
public class TestViewModel
{
public ObservableCollection<TestDepartament> Departaments { get; set; }
private ITestDataService _dataService;
public TestViewModel(ITestDataService dataService)
{
_dataService = dataService;
Departaments = new ObservableCollection<TestDepartament>();
}
public async Task RefreshItems()
{
var facultiesObservable = _dataService.GetDepartaments();
await facultiesObservable.ObserveOn(SynchronizationContext.Current).Do(items =>
{
Departaments.Clear();
foreach(var item in items)
Departaments.Add(item);
});
}
}
public interface ITestDataService
{
IObservable<IEnumerable<TestDepartament>> GetDepartaments();
}
public class MockDataService : ITestDataService
{
public IObservable<IEnumerable<TestDepartament>> GetDepartaments()
{
return Observable.Return(TestMockData.Departaments);
}
}
public static class TestMockData
{
public static List<TestDepartament> Departaments
{
get
{
var departaments = new List<TestDepartament>();
for (int i = 0; i < 15; i++)
{
departaments.Add(new TestDepartament
{
Name = $"Departament {i}",
ImageUrl = $"departament_{i}_image_url",
ContentUrl = $"departament_{i}_content_url",
});
}
return departaments;
}
}
}
public class TestDepartament
{
public string ContentUrl { get; set; }
public string Name { get; set; }
public string ImageUrl { get; set; }
}
}
这是xUnit测试:
public class DepartamentsViewModelTests
{
private readonly TestViewModel _viewModel;
public DepartamentsViewModelTests()
{
var dataService = new MockDataService();
_viewModel = new TestViewModel(dataService);
}
[Fact]
public async Task RefreshItemsTest()
{
await _viewModel.RefreshItems();
Assert.Equal(TestMockData.Departaments, _viewModel.Departaments);
}
}
答案 0 :(得分:2)
您是否打算在进行此刷新时阻止某些GUI。我是指通过显示微调器或其他非确定性进度条阻止用户进度?或者您是否希望用户能够继续做他们正在做的事情,但能够发出信号表明有一些后台进程正在发生,并在该过程完成时发出信号?
对于前者,我认为你拥有的当前签名是明智的,即你返回Task
。但是我认为可以改进实施。
public async Task RefreshItems()
{
var items = await _dataService.GetDepartaments()
.Take(1)
.ObserveOn(SynchronizationContext.Current)
.ToTask();
Departaments.ReplaceWithRange(items);
}
请注意Take(1)
。如果您尝试从IObservable<T>
转换为Task<T>
/ Task
,则只会获得序列完成时的最后一个值或产量。没有Take(1)
,我们可以永远等待。
但是,我认为您有一个从缓存加载的方案,因此可以获得0,1或2个OnNext调用。在这种情况下,我不知道等待最后一个值达到了什么?我还注意到缺少错误处理。
我可能会在我自己的代码中执行类似这样的FWIW
public void RefreshItems()
{
Departments.Clear();
_state = States.Processing();
var items = _dataService.GetDepartaments()
.SubscribeOn(_schedulerProvider.Background)
.ObserveOn(_schedulerProvider.Foreground)
.Subscribe(
item=> Departaments.Add(item),
ex => _state = States.Faulted(ex),
() => _state = States.Idle());
}
这允许
_states
字段/属性以反映当前正在进行的操作(处理,空闲或某些错误),Departments
列表中的项目,而不是一个大块,Task
和IObservable<T>
ObserveOn
但没有匹配的SubscribeOn
。修改强>
这是我要创建此视图模型的样式。我不想混合Task和IObservable。我也赞成使用Scheduler over Synchronization上下文。我添加了资源管理,以便订阅不重叠(如果多次调用Refresh),以便在ViewModel完成时可以取消订阅。这里应该很容易测试0,1或许多值。它还允许测试错误情况(例如,通过超时的OnError,网络中断等)。
我也只是为了好玩添加了Async State属性,以便外部消费者可以看到ViewModel当前正在处理某些内容。
然而,我确实过度测试有很多顾虑。我可能不会在实践中这样做,但我认为这样可以更容易阅读SO。http://share.linqpad.net/67hmc2.linq
void Main()
{
var schedulerProvider = new TestSchedulerProvider();
var cachedData = Enumerable.Range(0,3).Select(i => new TestDepartament
{
Name = $"Departament {i}",
ImageUrl = $"departament_{i}_image_url",
ContentUrl = $"departament_{i}_content_url",
}).ToArray();
var liveData = Enumerable.Range(10, 5).Select(i => new TestDepartament
{
Name = $"Departament {i}",
ImageUrl = $"departament_{i}_image_url",
ContentUrl = $"departament_{i}_content_url",
}).ToArray();
var data = schedulerProvider.Background.CreateColdObservable<IEnumerable<TestDepartament>>(
ReactiveTest.OnNext<IEnumerable<TestDepartament>>(100, cachedData),
ReactiveTest.OnNext<IEnumerable<TestDepartament>>(3000, liveData),
ReactiveTest.OnCompleted<IEnumerable<TestDepartament>>(3000));
var dataService = Substitute.For<ITestDataService>();
dataService.GetDepartaments().Returns(data);
var viewModel = new TestViewModel(dataService, schedulerProvider);
Assert.Equal(AsyncState.Idle, viewModel.State);
viewModel.RefreshItems();
Assert.Equal(AsyncState.Processing, viewModel.State);
schedulerProvider.Background.AdvanceTo(110);
schedulerProvider.Foreground.Start();
Assert.Equal(cachedData, viewModel.Departments);
schedulerProvider.Background.Start();
schedulerProvider.Foreground.Start();
Assert.Equal(liveData, viewModel.Departments);
Assert.Equal(AsyncState.Idle, viewModel.State);
}
// Define other methods and classes here
public class TestViewModel : INotifyPropertyChanged, IDisposable
{
private readonly ITestDataService _dataService;
private readonly ISchedulerProvider _schedulerProvider;
private readonly SerialDisposable _refreshSubscription = new SerialDisposable();
private AsyncState _state = AsyncState.Idle;
public ObservableCollection<TestDepartament> Departments { get;} = new ObservableCollection<UserQuery.TestDepartament>();
public AsyncState State
{
get { return _state; }
set
{
_state = value;
OnPropertyChanged(nameof(State));
}
}
public TestViewModel(ITestDataService dataService, ISchedulerProvider schedulerProvider)
{
_dataService = dataService;
_schedulerProvider = schedulerProvider;
}
public void RefreshItems()
{
Departments.Clear();
State = AsyncState.Processing;
_refreshSubscription.Disposable = _dataService.GetDepartaments()
.SubscribeOn(_schedulerProvider.Background)
.ObserveOn(_schedulerProvider.Foreground)
.Subscribe(
items =>
{
Departments.Clear();
foreach (var item in items)
{
Departments.Add(item);
}
},
ex => State = AsyncState.Faulted(ex.Message),
() => State = AsyncState.Idle);
}
#region INotifyPropertyChanged implementation
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
public void Dispose()
{
_refreshSubscription.Dispose();
}
}
public interface ITestDataService
{
IObservable<IEnumerable<TestDepartament>> GetDepartaments();
}
public interface ISchedulerProvider
{
IScheduler Foreground { get;}
IScheduler Background { get;}
}
public class TestSchedulerProvider : ISchedulerProvider
{
public TestSchedulerProvider()
{
Foreground = new TestScheduler();
Background = new TestScheduler();
}
IScheduler ISchedulerProvider.Foreground { get { return Foreground; } }
IScheduler ISchedulerProvider.Background { get { return Background;} }
public TestScheduler Foreground { get;}
public TestScheduler Background { get;}
}
public sealed class AsyncState
{
public static readonly AsyncState Idle = new AsyncState(false, null);
public static readonly AsyncState Processing = new AsyncState(true, null);
private AsyncState(bool isProcessing, string errorMessage)
{
IsProcessing = isProcessing;
IsFaulted = string.IsNullOrEmpty(errorMessage);
ErrorMessage = ErrorMessage;
}
public static AsyncState Faulted(string errorMessage)
{
if(string.IsNullOrEmpty(errorMessage))
throw new ArgumentException();
return new AsyncState(false, errorMessage);
}
public bool IsProcessing { get; }
public bool IsFaulted { get; }
public string ErrorMessage { get; }
}
public class TestDepartament
{
public string ContentUrl { get; set; }
public string Name { get; set; }
public string ImageUrl { get; set; }
}
答案 1 :(得分:0)
您的代码正在为可观察对象创建两个订阅 - 有时departamentsObservable.ObserveOn(SynchronizationContext.Current).Subscribe(...)
会在 await departamentsObservable.FirstOrDefaultAsync()
完成之前生成值,有时不会&# 39;吨强>
两个订阅意味着源运行的两个独立时间。当await
快速完成时,您的其他订阅不会被调用(或在调用Assert.Equal
后调用),因此值不会添加到列表中。
请改为尝试:
public async Task RefreshItems()
{
var departamentsObservable = _dataService.GetDepartaments();
await departamentsObservable.ObserveOn(SynchronizationContext.Current).Do(items =>
{
Departaments.ReplaceWithRange(items);
});
}
现在您只有一个订阅等待生成的最后一个值。
如果您只期望一个值,并且可观察量不会自然结束,那么最后会弹出一个.Take(1)
。
答案 2 :(得分:0)
@lee-campbell的答案很棒,但缺少Xamarin Forms实现的ISchedulerProvider
。
以下是我使用的内容:
public sealed class SchedulerProvider : ISchedulerProvider
{
public IScheduler Foreground => new SynchronizationContextScheduler(SynchronizationContext.Current);
public IScheduler Background => TaskPoolScheduler.Default;
}