Mocking EF Context for Unit Testing WCF Services
Units tests are all about testing a specific unit of code without any external dependencies. This makes the tests faster and less fragile, since there are no out-of-process calls and all dependencies are under the test's control. Of course, it's not always easy to remove all external dependencies. One such example is a WCF service using entity framework for database access in its operations.
It would be easy to create a test calling such a web service through a proxy (i.e. a service reference) while it is connected to an appropriate sample database. Though, that would be an integration test, not a unit test. Such tests are slow, it's difficult to setup the environment for them to run correctly, and they tend to break easily because something happened to the database or the hosted service. Wouldn't it be nice to be able to test a service without the database and without having to host it at all? Let's see how this can be done.
Our sample DbContext
will have only a single entity:
public class ServiceContext : DbContext
{
public DbSet<ErrorReportEntity> ErrorReports { get; set; }
}
public class ErrorReportEntity
{
public int Id { get; set; }
public string ExceptionDetails { get; set; }
public DateTime OccuredAt { get; set; }
public DateTime ReportedAt { get; set; }
}
The sample service will have only a single method:
[ServiceContract]
public interface IService
{
[OperationContract]
void ReportError(ErrorReport report);
}
public class Service : IService
{
public void ReportError(ErrorReport report)
{
using (var context = new ServiceContext())
{
var reportEntity = new ErrorReportEntity
{
ExceptionDetails = report.ExceptionDetails,
OccuredAt = report.OccuredAt,
ReportedAt = DateTime.Now,
};
context.ErrorReports.Add(reportEntity);
context.SaveChanges();
}
}
}
We first need an alternative implementation of ServiceContext
for testing which won't require a database. This could be its interface:
public interface IServiceContext : IDisposable
{
IDbSet<ErrorReportEntity> ErrorReports { get; set; }
void SaveChanges();
}
Notice the use of IDbSet
instead of DbSet
. We also added SaveChanges
to the interface since we need to call it from our service. ServiceContext
now needs to implement this interface:
public class ServiceContext : DbContext, IServiceContext
{
public IDbSet<ErrorReportEntity> ErrorReports { get; set; }
public new void SaveChanges()
{
base.SaveChanges();
}
}
For the tests we will of course have a different implementation.
public class MockServiceContext : IServiceContext
{
public IDbSet<ErrorReportEntity> ErrorReports { get; set; }
public MockServiceContext()
{
ErrorReports = new InMemoryDbSet<ErrorReportEntity>();
}
public void SaveChanges()
{ }
public void Dispose()
{ }
}
You might wonder where InMemoryDbSet
came from. It's an in-memory implementation of IDbSet
which you can get by installing FakeDbSet NuGet package.
Having two different implementations of IServiceContext
, we need a way to inject the desired one into our service for each case: MockServiceContext
when testing and ServiceContext
when actually hosting the service in IIS. We'll use Ninject as the dependency injection framework with the constructor injection pattern. This would be the naïve attempt at changing the service implementation:
public class Service : IService
{
private readonly IServiceContext _context;
public Service(IServiceContext context)
{
_context = context;
}
public bool ReportError(ErrorReport report)
{
var reportEntity = new ErrorReportEntity
{
ExceptionDetails = report.ExceptionDetails,
OccuredAt = report.OccuredAt,
ReportedAt = DateTime.Now,
};
_context.ErrorReports.Add(reportEntity);
_context.SaveChanges();
}
}
The downside of this approach is that we have changed the behavior. Instead of creating a new DbContext
for each method call, we now use the same instance for the complete lifetime of the service. We'll see how to fix that later. First we need to make sure that we always pass the correct IServiceContext
implementation to the constructor. In the test we'll do it manually:
[TestMethod]
public void ValidReport()
{
var context = new MockServiceContext();
var service = new Service(context);
var error = new ErrorReport { /* initialize values */ };
service.ReportError(error);
var errorFromDb = context.ErrorReports
.Single(e => e.OccuredAt == error.OccuredAt);
// assert property values
}
For hosting in ISS we'll take advantage of WCF extensions for Ninject. NuGet package installation among other things also adds a NinjectWebCommon.cs
file in App_Start
folder. We need to open it and add the following line of code to the RegisterServices
method inside it to register the correct IServiceContext
implementation with the Ninject kernel:
kernel.Bind<IServiceContext>().To<ServiceContext>();
We only need to add the Ninject factory to the service declaration in Service.svc
file, and the Service
class will be correctly created – Ninject will pass it an instance of ServiceContext
:
<%@ ServiceHost Language="C#"
Service="WebService.Service"
CodeBehind="Service.svc.cs"
Factory="Ninject.Extensions.Wcf.NinjectServiceHostFactory" %>
Now it's time to address the already mentioned issue of not instantiating a new ServiceContext
for each method call. Ninject.Extensions.Factory NuGet package can help us with that. It will allow us to pass a ServiceContext
factory to the service instead of passing it an already created ServiceContext
. We first need a factory interface:
public interface IServiceContextFactory
{
IServiceContext CreateContext();
}
Now we can change DbContext
handling in Service
back to the way it originally was:
public class Service : IService
{
private readonly IServiceContextFactory _contextFactory;
public Service(IServiceContextFactory contextFactory)
{
_contextFactory = contextFactory;
}
public bool ReportError(ErrorReport report)
{
using (var context = _contextFactory.CreateContext())
{
var reportEntity = new ErrorReportEntity
{
ExceptionDetails = report.ExceptionDetails,
OccuredAt = report.OccuredAt,
ReportedAt = DateTime.Now,
};
context.ErrorReports.Add(reportEntity);
context.SaveChanges();
}
}
}
For this to work when service is hosted in IIS, we need to additionally register the factory in RegisterServices
:
kernel.Bind<IServiceContextFactory>().ToFactory();
For the test we need to implement the factory ourselves – it only takes a couple of lines:
public class MockServiceContextFactory : IServiceContextFactory
{
public IServiceContext Context { get; private set; }
public MockServiceContextFactory()
{
Context = new MockServiceContext();
}
public IServiceContext CreateContext()
{
return Context;
}
}
At the beginning of the test we now create a factory instead of the context directly:
var contextFactory = new MockServiceContextFactory();
var service = new Service(contextFactory);
That's it. With some pretty simple refactoring we managed to get rid of all hard-coded external dependencies in our service. Writing tests is now much simpler and there is almost no code overhead when additional members are added to DbContext
or the service contract.