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:
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.
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:
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:
[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.
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.
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.
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
:
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:
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.