I'm a fan of CQRS. It's a great way to separate concerns when dealing with data. In fact, I'm a pretty big fan of the command pattern as a whole. One of the big issues I've faced with using this pattern is unit testing the queries. (Note: this is a pretty contrived example, but it should get the idea across.) Sample code can be found on Github.
The Problem
I want to implement some kind of cache object, and I want to use CQRS in fetching the entities from the database. To top it off, I want to unit test the cache. Here's a look at the cache (and yes, I know it's not really caching anything):
public class ReportCache { private readonly ISession session; private readonly GetReportByDateAndType getReportByDateAndType; public ReportCache(ISession session, GetReportByDateAndType getReportByDateAndType) { this.session = session; this.getReportByDateAndType = getReportByDateAndType; } public Report GetReportByDateAndType(DateTime reportDate, ReportType reportType) { getReportByDateAndType.ReportDate = reportDate; getReportByDateAndType.ReportType = reportType; var reports = getReportByDateAndType.Execute(session); return reports.First(); } }
The Solution
The hurdle is how to mock out the query object. I've always tried to implement some kind of interface with varying levels of success. Following that approach made it difficult to use/inject the query into whatever object was using it.
I was usually left with an interface supplied in the constructor which had to be downcast to allow access to the properties. Another option would be to supply a factory object, which could create the appropriate query when necessary. The former being a Liskov Substitution Principle violation. The latter adding unnecessary complexity.
One day, I realized I could use abstract classes, and life became a little easier. Let's say we start with the query itself:
public class GetReportByDateAndType : QueryBase { public DateTime ReportDate { get; set; } public ReportType ReportType { get; set; } public override IQueryable<Report> Execute(ISession session) { return session.Query<Report>() .Where(report => report.ReportDate == ReportDate && report.Type == ReportType); } }
You'll notice that the queries inherit from a base class. This class is an abstract, and enforces that the Execute() method can be overridden. I'm using this similar to an interface. It's a means to define what the queries will look like:
public abstract class QueryBase { public abstract IQueryable<Report> Execute(ISession session); }
Doing a unit test with this setup, becomes a matter of simply mocking the query. This can be done with a framework or a hand-rolled mock. The following is an example using NSubstitute:
public static class ReportCacheTests { public class ReportCacheSpecs : ContextSpecification { protected ReportCache Sut; protected GetReportByDateAndType TestQuery; protected ISession TestSession; protected override void Context() { TestSession = Substitute.For<ISession>(); TestQuery = Substitute.For<GetReportByDateAndType>(); Sut = new ReportCache(TestSession, TestQuery); } } [TestClass] public class MockWithNSubstitute : ReportCacheSpecs { private Report expectedReport; private Report result; private DateTime testDate; private ReportType testReportType; protected override void Context() { base.Context(); testDate = new DateTime(2012, 01, 02); testReportType = ReportType.Cost; expectedReport = new Report { ReportId = 42L, }; TestQuery.Execute(TestSession) .Returns(new List<Report> { expectedReport }.AsQueryable()); } protected override void BecauseOf() { result = Sut.GetReportByDateAndType(testDate, testReportType); } [TestMethod] public void TheCacheShouldReturnTheFirstReport() { result.Should().Be(expectedReport); } [TestMethod] public void TheCorrectParametersShouldBeSetOnTheQuery() { TestQuery.ReportDate.Should().Be(testDate); TestQuery.ReportType.Should().Be(testReportType); } } }
I've posted an example to Github. You'll notice the example also shows how to extend the functionality of the query without using inheritance. I plan on writing a blog about this at a later date. An added bonus is any IoC container can be used to supply the query, so there's no need for a factory.
No comments:
Post a Comment