Silverlight ViewModel 单元测试 (RIA Services)





5.00/5 (3投票s)
本文介绍了如何使用模拟的 DomainContext 为 Silverlight ViewModels 编写单元测试。
引言
自异步 DomainContext
(在 ViewModels 中用于访问服务方法)无法使用常用方法进行模拟以来,使用 RIA 服务后端对 Silverlight ViewModels 进行单元测试一直是一个真正的问题。
在本文中,描述了一种不需要任何额外库和依赖项,并且可以创建可理解的单元测试的方法。
这种方法基于 Brian Noyes 的文章,事实上只是扩展了它,以使事情更有效。
单元测试示例
以下示例演示了几个单元测试,以展示该方法的基本用法。
假设有一个特定的 ViewModel 需要测试
public class UserListViewModel
{
public RiaDomainContext DomainContext { get; set; }
public UserEntity SelectedUser { get; set; }
public ObservableCollection<UserEntity> UserList { get; set; }
public void Initialize()
{
UpdateUserList();
}
public void ProcessSelectedUser()
{
DomainContext.ProcessUser(SelectedUser);
}
public void RemoveSelectedUser()
{
DomainContext.UserEntities.Remove(SelectedUser);
DomainContext.SubmitChanges(lo => UpdateUserList(), null);
}
public void UpdateUserList()
{
UserList = new ObservableCollection<UserEntity>();
var getUsersQuery = DomainContext.GetUsersQuery();
DomainContext.Load(
getUsersQuery,
LoadBehavior.RefreshCurrent,
lo =>
{
UserList = new ObservableCollection<UserEntity>(lo.Entities);
},
null);
}
}
现在让我们使用 Arrange-Act-Assert 方法对其进行测试。
Arrange
过程初始化测试依赖项。
protected override void Arrange()
{
base.Arrange();
TestService.SetupSequence(
service =>
service.BeginGetUsers(AnyCallback, AnyObject))
.Returns(EntityList(GetTestUsers()))
.Returns(EntityList(GetTestUsersAfterRemoveUser()))
.Returns(EntityList(GetTestUsersAfterProcessUser()));
TestService.Setup(
service =>
service.BeginSubmitChanges(AnyChangeset, AnyCallback, AnyObject))
.Returns(EmptyEntityList);
TestService.Setup(
service =>
service.BeginProcessUser(It.IsAny<UserEntity>(), AnyCallback, AnyObject))
.Returns(EmptyEntityList);
TestViewModel.Initialize();
}
在此过程中,TestService
被初始化以响应 GetUsers
、SubmitChanges
和 ProcessUser
请求返回特定的实体集。
TestService
– 只是从 RIA 服务生成的代码中提取的IRiaDomainServiceContract
接口的模拟。TestViewModel
– 是被测 ViewModel 的一个实例。AnyCallback
和AnyObject
– 常量对象,它们对测试不重要,但IRiaDomainServiceContract
接口需要它们。
Act 过程用于对测试对象执行一些操作
protected override void Act()
{
base.Act();
TestViewModel.SelectedUser = TestViewModel
.UserList.First(
user => user.ID == _userToDelete);
TestViewModel.RemoveSelectedUser();
TestViewModel.SelectedUser = TestViewModel
.UserList.First(
user => user.ID == _userToProcess);
TestViewModel.ProcessSelectedUser();
}
Asserts 位于测试方法中
[TestMethod]
public void LoadsUsers()
{
TestService.Verify(vm =>
vm.BeginGetUsers(AnyCallback, AnyObject),
Times.AtLeastOnce());
}
[TestMethod]
public void RemovesSelectedUser()
{
TestService.Verify(
vm => vm.BeginSubmitChanges(ChangeSetContains<UserEntity>(
user => user.ID == _userToDelete)
,AnyCallback
,AnyObject),
Times.Once());
}
[TestMethod]
public void ProcessesSelectedUser()
{
TestService.Verify(
vm => vm.BeginProcessUser(It.Is<UserEntity>(
user => user.ID == _userToProcess)
, AnyCallback
, AnyObject),
Times.Once());
}
这些断言验证在 Act 阶段期间,GetUsers
、SubmitChanges
和 ProcessUser
方法是否使用特定参数被调用。
测试引擎
现在让我们深入了解这些测试的背景。
正如 Brian Noyes 在他的文章中所建议的,模拟 DomainClient
而不是 DomainContext
(DomainClient
可以作为构造函数参数传递给 DomainContext
)更为合理。使用这种方法,仅模拟域客户端异步响应,而不会影响任何 DomainContext
的预请求或后请求操作。
public abstract class TestDomainClient : DomainClient
{
private SynchronizationContext _syncContext;
protected TestDomainClient()
{
_syncContext = SynchronizationContext.Current;
}
protected override sealed IAsyncResult BeginInvokeCore(InvokeArgs invokeArgs,
AsyncCallback callback, object userState) {}
protected override sealed InvokeCompletedResult EndInvokeCore(IAsyncResult asyncResult) {}
protected override sealed IAsyncResult BeginQueryCore(EntityQuery query,
AsyncCallback callback, object userState) {}
protected override sealed QueryCompletedResult EndQueryCore(IAsyncResult asyncResult) {}
protected override sealed IAsyncResult BeginSubmitCore(EntityChangeSet changeSet,
AsyncCallback callback, object userState) {}
protected override sealed SubmitCompletedResult EndSubmitCore(IAsyncResult asyncResult) {}
}
所有 TestDomainClient
的 'BeginXxx' 方法都返回 IAsyncResult
,其中包含请求完成时的实际结果。
重写这些方法可以传递任何类型的响应结果给 DomainContext
请求,因此可以使用自定义测试方法模拟 DomainContext
请求处理程序。
这里唯一的问题是 BeginInvokeCore
和 BeginQueryCore
方法没有反映实际的查询名称,因此不适合在测试中使用。
事实证明,在生成的 RiaDomainContext
类中有一个接口 IRiaDomainServiceContract
,它包含所有可用查询方法的签名。
public interface IRiaDomainServiceContract
{
IAsyncResult BeginGetUsers(AsyncCallback callback, object asyncState);
QueryResult<UserEntity> EndGetUsers(IAsyncResult result);
IAsyncResult BeginProcessUser(UserEntity entity, AsyncCallback callback, object asyncState);
void EndProcessUser(IAsyncResult result);
IAsyncResult BeginSubmitChanges(IEnumerable<ChangeSetEntry> changeSet,
AsyncCallback callback, object asyncState);
IEnumerable<ChangeSetEntry> EndSubmitChanges(IAsyncResult result);
}
因此,可以在 BeginInvokeCore
和 BeginQueryCore
方法中使用 IRiaDomainServiceContract
来生成 IAsyncResult
。IRiaDomainServiceContract
可以注入到 TestDomainClient
中,并像普通接口一样进行模拟。
这就是我们在 TestDomainClient
中得到的结果
private IRiaDomainServiceContract _serviceContract;
protected override sealed IAsyncResult BeginInvokeCore(InvokeArgs invokeArgs,
AsyncCallback callback, object userState)
{
MethodInfo methodInfo = ResolveBeginMethod(invokeArgs.OperationName);
ParameterInfo[] parameters = methodInfo.GetParameters();
.....
var asyncResult = (TestAsyncResult)methodInfo.Invoke(_serviceContract, parameters);
return asyncResult;
}
protected override sealed IAsyncResult BeginQueryCore(
EntityQuery query, AsyncCallback callback, object userState)
{
MethodInfo methodInfo = ResolveBeginMethod(query.QueryName);
ParameterInfo[] parameters = methodInfo.GetParameters();
.....
var asyncResult = (TestAsyncResult)methodInfo.Invoke(_serviceContract, parameters);
return asyncResult;
}
ResolveBeginMethod
来自 .NET WebDomainClient
实现。
之后,可以使用模拟的 IRiaDomainServiceContract
来处理对 DomainClient
(因此也对 DomainContext
)的所有请求。
唯一明显的不便是在每个方法的末尾传递 AnyCallback
和 AnyObject
,因为所有 IRiaDomainServiceContract
签名都包含这些参数作为必需参数。
测试环境
现在让我们设置测试环境。
这可能看起来像这样
public abstract class RiaContextTestEnvironment<T> : ContextBase where T : ViewModelBase
{
protected AsyncCallback AnyCallback
{
get
{
return It.IsAny<AsyncCallback>();
}
}
protected IEnumerable<ChangeSetEntry> AnyChangeset
{
get
{
return It.IsAny<IEnumerable<ChangeSetEntry>>();
}
}
protected object AnyObject
{
get
{
return It.IsAny<object>();
}
}
protected Mock<RiaDomainContext.IRiaDomainServiceContract> TestService { get; set; }
protected T TestViewModel { get; set; }
protected IEnumerable<ChangeSetEntry> ChangeSetContains<T2>(
Func<T2, bool> match) where T2 : Entity
{
return
It.Is<IEnumerable<ChangeSetEntry>>(
changes =>
changes.Any(
change =>
change.Entity is T2 && match((T2)change.Entity)));
}
protected TestAsyncResult EntityList(IEnumerable<Entity> entityList)
{
return new TestAsyncResult(entityList);
}
protected TestAsyncResult EmptyEntityList()
{
return new TestAsyncResult(new List<Entity>());
}
protected override void Arrange()
{
base.Arrange();
SynchronizationContext.SetSynchronizationContext(new SynchronizationContextSerial());
var dcx = new Mock<RiaDomainContext.IRiaDomainServiceContract>(MockBehavior.Loose);
var testRiaContext = new RiaDomainContext(
new TestDomainClient<RiaDomainContext.IRiaDomainServiceContract>(dcx.Object));
TestService = dcx;
var testViewModel = GetTestViewModel();
testViewModel.DomainContext = testRiaContext;
TestViewModel = testViewModel;
}
protected abstract T GetTestViewModel();
}
在 arrange 过程中,模拟的 IRiaDomainServiceContract
被注入到 TestDomainClient
中。然后 TestDomainClient
被注入到 RiaDomainContext
中。
现在可以在 IRiaDomainServiceContract
模拟上指定请求处理程序,这些处理程序随后将确定 DomainContext
的行为。
就是这样 - DomainContext
被模拟,ViewModel 代码被隔离。而且,它将始终与服务接口保持同步。
RIA 服务测试中的重要要求是使用串行同步上下文,因为 DomainContext
使用同步上下文来执行内部操作。如果未设置串行上下文,则某些代码将异步执行。不幸的是,有时无法重写上下文,因为它标有 SecurityCritical
属性。
关注点
我相信有方法可以简化代码并摆脱不必要的参数。
此外,一个大问题是关于 SynchronizationContext
。我想知道是否有可能避免重写它以使测试串行化。