Click here to Skip to main content
15,885,546 members
Articles / Programming Languages / C#
Article

Silverlight ViewModel Unit Testing (RIA Services)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
16 Oct 2013CPOL3 min read 15.1K   119   2  
This article describes how to write Unit Tests for Silverlight ViewModels with a mocked DomainContext.

Introduction

Unit Testing of Silverlight ViewModels with the RIA Services back-end has been a real problem since the asynchronous DomainContext which is used in the ViewModels to access service methods can’t be mocked with usual methods.

In this article the approach is described which doesn’t require any additional libraries and dependences and makes it possible to create understandable unit tests.

This approach is based on Brian Noyes article, and in fact just extends it to make things more effective:

Unit Test Sample

The following examples demonstrate a few unit tests to present a basic usage of the approach.

Suppose there is a particular ViewModel to test:

C#
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);
    }
}

Now let’s test it with the Arrange-Act-Assert approach.

Arrange procedure initializes test dependences.

C#
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();
}

In this procedure TestService is initialized to return specific entity sets in response to GetUsers, SubmitChanges, and ProcessUser requests.

  • TestService is just a mock of the IRiaDomainServiceContract interface taken from the RIA Services generated code.
  • TestViewModel – is an instance of the ViewModel under test.
  • AnyCallback and AnyObject – constant objects which are not important for tests but are required by the IRiaDomainServiceContract interface.

Act procedure is used to perform some actions over the test object:

C#
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 are located in test methods:

C#
[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());
}

These asserts verify that GetUsers, SubmitChanges, and ProcessUser methods are invoked with particular parameters during the Act stage.

Image 1

Test Engine

Now let’s deep into the background of these tests.

As Brian Noyes suggests in his article, it’s more reasonable to mock DomainClient instead of DomainContext (DomainClient can be passed as a constructor parameter to DomainContext). With this approach only domain client async responses are mocked without affecting any DomainContext pre- or post-request operations.

C#
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) {}
}

All of TestDomainClient ‘BeginXxx’ methods return IAsyncResult which contains the actual result when the request is completed.

Overriding this methods makes it possible to pass any kind of result in response to DomainContext requests and therefore mock DomainContext request handlers with custom test methods.

The only problem here is that BeginInvokeCore and BeginQueryCore methods don’t reflect actual query names and therefore are not suitable enough to use in tests.

It turned out that there is an interface IRiaDomainServiceContract which is located inside the generated RiaDomainContext class and contains signatures of all available query methods.

C#
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);
}

Thus it’s possible to use IRiaDomainServiceContract inside the BeginInvokeCore and BeginQueryCore methods to generate IAsyncResult. IRiaDomainServiceContract can be injected into TestDomainClient and mocked as a regular interface.

Here what we’ve got in the result in TestDomainClient:

C#
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 is taken from .NET WebDomainClient implementation.

Afterwards it’s possible to use mocked IRiaDomainServiceContract to handle all requests to DomainClient (and therefore to DomainContext).

The only obvious inconvenience is to pass AnyCallback and AnyObject at the end of each method since all of the IRiaDomainServiceContract signatures contain these parameters as required.

Test Environment

Now let’s set up the test environment.

Here how it may look like:

C#
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();
}

In arrange procedure mocked IRiaDomainServiceContract is injected into TestDomainClient. Then TestDomainClient is injected into RiaDomainContext.

Now it’s possible to specify request handlers on IRiaDomainServiceContract mock which will then determine the DomainContext behavior.

That’s it – the DomainContext is mocked and ViewModel code is isolated. Moreover, it will always be up to date with the service interface.

The important requirement in RIA Services tests is to use serial synchronization context, since DomainContext uses synchronization context to perform internal operations. In case serial context is not set, some code will perform asynchronously. Unfortunately, sometimes it’s impossible to override the context since it is marked with SecurityCritical attribute.

Points of Interest

I’m sure there are ways to simplify the code and get rid of unnecessary parameters.

Also the big question is about SynchronizationContext. I wonder if it’s possible to avoid overriding it to make tests serial.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer One Inc.
Ukraine Ukraine
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --