How can I mock a DataServiceQuery for unit testing purpose?
Long Details follow: Imagine an ASP.NET MVC application, where the controller talks to an ADO.NET DataService that encapsulates the storage of our models (for example sake we'll be reading a list of Customers). With a reference to the service, we get a generated class inheriting from DataServiceContext:
namespace Sample.Services
{
public partial class MyDataContext : global::System.Data.Services.Client.DataServiceContext
{
public MyDataContext(global::System.Uri serviceRoot) : base(serviceRoot) { /* ... */ }
public global::System.Data.Services.Client.DataServiceQuery<Customer> Customers
{
get
{
if((this._Customers==null))
{
this._Customers = base.CreateQuery<Customer>("Customers");
}
return this._Customers;
}
}
/* and many more members */
}
}
The Controller could be:
namespace Sample.Controllers
{
public class CustomerController : Controller
{
private IMyDataContext context;
public CustomerController(IMyDataContext context)
{
this.context=context;
}
public ActionResult Index() { return View(context.Customers); }
}
}
As you can see, I used a constructor that accepts an IMyDataContext instance so that we can use a mock in our unit test:
[TestFixture]
public class TestCustomerController
{
[Test]
public void Test_Index()
{
MockContext mockContext = new MockContext();
CustomerController controller = new CustomerController(mockContext);
var customersToReturn = new List<Customer>
{
new Customer{ Id=1, Name="Fred" },
new Customer{ Id=2, Name="Wilma" }
};
mockContext.CustomersToReturn = customersToReturn;
var result = controller.Index() as ViewResult;
var models = result.ViewData.Model;
//Now we have to compare the Customers in models with those in customersToReturn,
//Maybe by loopping over them?
foreach(Customer c in models) //*** LINE A ***
{
//TODO: compare with the Customer in the same position from customersToreturn
}
}
}
MockContext and MyDataContext need to implement the same interface IMyDataContext:
namespace Sample.Services
{
public interface IMyDataContext
{
DataServiceQuery<Customer> Customers { get; }
/* and more */
}
}
However, when we try and implement the MockContext class, we run into problems due to the nature of DataServiceQuery (which, to be clear, we're using in the IMyDataContext interface simply because that's the data type we found in the auto-generated MyDataContext class that we started with). If we try to write:
public class MockContext : IMyDataContext
{
public IList<Customer> CustomersToReturn { set; private get; }
public DataServiceQuery<Customer> Customers { get { /* ??? */ } }
}
In the Customers getter we'd like to instantiate a DataServiceQuery instance, populate it with the Customers in CustomersToReturn, and return it. The problems I run into:
1~ DataServiceQuery has no public constructor; to instantiate one you should call CreateQuery on a DataServiceContext; see MSDN
2~ If I make the MockContext inherit from DataServiceContext as well, and call CreateQuery to get a DataServiceQuery to use, the service and query have to be tied to a valid URI and, when I try to iterate or access the objects in the query, it will try and execute against that URI. In other words, if I change the MockContext as such:
namespace Sample.Tests.Controllers.Mocks
{
public class MockContext : DataServiceContext, IMyDataContext
{
public MockContext() :base(new Uri("http://www.contoso.com")) { }
public IList<Customer> CustomersToReturn { set; private get; }
public DataServiceQuery<Customer> Customers
{
get
{
var query = CreateQuery<Customer>("Customers");
query.Concat(CustomersToReturn.AsEnumerable<Customer>());
return query;
}
}
}
}
Then, in the unit test, we get an error on the line marked as LINE A, because http://www.contoso.com doesn't host our service. The same error is triggered even if LINE A tries to get the number of elements in models. Thanks in advance.
I solved this by creating an interface IDataServiceQuery
with two implementations:
DataServiceQueryWrapper
MockDataServiceQuery
I then use IDataServiceQuery
wherever I would have previously used a DataServiceQuery
.
public interface IDataServiceQuery<TElement> : IQueryable<TElement>, IEnumerable<TElement>, IQueryable, IEnumerable
{
IDataServiceQuery<TElement> Expand(string path);
IDataServiceQuery<TElement> IncludeTotalCount();
IDataServiceQuery<TElement> AddQueryOption(string name, object value);
}
The DataServiceQueryWrapper
takes a DataServiceQuery
in it's constructor and then delegates all functionality to the query passed in. Similarly, the MockDataServiceQuery
takes an IQueryable
and delegates everything it can to the query.
For the mock IDataServiceQuery
methods, I currently just return this
, though you could do something to mock the functionality if you want to.
For example:
// (in DataServiceQueryWrapper.cs)
public IDataServiceQuery<TElement> Expand(string path)
{
return new DataServiceQueryWrapper<TElement>(_query.Expand(path));
}
// (in MockDataServiceQuery.cs)
public IDataServiceQuery<TElement> Expand(string path)
{
return this;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With