65.9K
CodeProject 正在变化。 阅读更多。
Home

Silverlight ViewModel 单元测试 (RIA Services)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2013年10月16日

CPOL

3分钟阅读

viewsIcon

15600

downloadIcon

144

本文介绍了如何使用模拟的 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 被初始化以响应 GetUsersSubmitChangesProcessUser 请求返回特定的实体集。

  • TestService 只是从 RIA 服务生成的代码中提取的 IRiaDomainServiceContract 接口的模拟。
  • TestViewModel – 是被测 ViewModel 的一个实例。
  • AnyCallbackAnyObject – 常量对象,它们对测试不重要,但 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 阶段期间,GetUsersSubmitChangesProcessUser 方法是否使用特定参数被调用。

测试引擎

现在让我们深入了解这些测试的背景。

正如 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 请求处理程序。

这里唯一的问题是 BeginInvokeCoreBeginQueryCore 方法没有反映实际的查询名称,因此不适合在测试中使用。

事实证明,在生成的 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);
}

因此,可以在 BeginInvokeCoreBeginQueryCore 方法中使用 IRiaDomainServiceContract 来生成 IAsyncResultIRiaDomainServiceContract 可以注入到 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)的所有请求。

唯一明显的不便是在每个方法的末尾传递 AnyCallbackAnyObject,因为所有 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。我想知道是否有可能避免重写它以使测试串行化。

© . All rights reserved.