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.