Introduction
Unit testing business logic that is based on Entity Framework is a difficult task. The aim of Unit
Testing is to test the business logic in isolation without dependencies on other components of the system.
The problem with Entity Framework is that it depends on the existance of a real database to run correctly.
A common approach for unit testing is to abstract away Entity Framework classes (such as DbContext) and run the
business logic under test against mocked objects in memory.
However, there are a few problems with this approach:
-
Running Linq expressions with mocked objects may produce different results compared
to when running against a real database. Some operations may throw exceptions,
so a passing Unit test doesn't necessarily mean the code will work in production.
-
Mocking up test data can be a lot of work. For complex schemas where there are
lots of relationships between entity types, these references need to be hand-crafted by setting appropriate Navigation Properties manually.
-
When running against a real database, the
DbSet.Include can
configure Entity Framework to automatically load related entities. This method is will not work
if you are using mocked objects.
This article shows two different approaches for testing Entity Framework that reduces these problems.
1. Effort
http://effort.codeplex.com/
This project is an Entity Framework provider that runs in memory. You can still use your
DbContext or ObjectContext classes within unit tests, without having to have an actual
database.
Many of the features of Entity Framework will still work. Navigation Properties will automatically be loaded
with related entities and the DbSet.Include
method also works as it would if connected to an real database.
A draw back of this approach is that we are still not running against a real database, but instead a different Entity Framework provider.
Some operations do not behave as they would against a real database. In fact, at the time of writing, Effort does not support
some operations that are supported by other database providers. (See this issue on CodePlex).
Essentially, this approach helps to solve points 2 and 3 above, but not 1.
2. SQL CE
This approach is using a real database. This article will show you how to create SQL CE databases on-the-fly to run your tests
against. SQL CE also supports Entity Framework Code First, so the database schema is also generated automatically from your
entity model.
It's worth pointing out that this approach is likely to be slower than testing in memory since SQL CE is a fully blown database and operations will write to disk as opposed to just working within memory.
An Example Entity Framework model
In this example, the Entity Model contains "Product" and "Tag" entities, which have a
many-to-many relationship between the them. This relationship is facilitated with a "ProductTag" entity,
that relates a Product with a Tag:
public partial class Product
{
public System.Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public Nullable<decimal> Price { get; set; }
public System.DateTime CreatedDate { get; set; }
public virtual ICollection<ProductTag> ProductTags { get; set; }
}
public partial class Tag
{
public System.Guid Id { get; set; }
public string Name { get; set; }
public System.DateTime CreatedDate { get; set; }
public virtual ICollection<ProductTag> ProductTags { get; set; }
}
public partial class ProductTag
{
public System.Guid Id { get; set; }
public System.Guid ProductId { get; set; }
public System.Guid TagId { get; set; }
public System.DateTime CreatedDate { get; set; }
public virtual Product Product { get; set; }
public virtual Tag Tag { get; set; }
}
I also have a MyAppContext class that inherits from DbContext.
public partial class MyAppContext : DbContext
{
public MyAppContext()
: base("name=MyAppContext")
{
this.Configuration.LazyLoadingEnabled = false;
}
public MyAppContext(string connectionString)
: base(connectionString)
{
this.Configuration.LazyLoadingEnabled = false;
}
public MyAppContext(DbConnection connection)
: base(connection, true)
{
this.Configuration.LazyLoadingEnabled = false;
}
public IDbSet<Product> Products { get; set; }
public IDbSet<ProductTag> ProductTags { get; set; }
public IDbSet<Tag> Tags { get; set; }
}
Example business logic and Unit Test
In order to demonstrate the different test approaches, I need some code to put under test.
I have ProductRepository class with a method GetProductsByTagName that gets all Product entities
that are tagged with the specified tagName. The class constructor takes a MyAppContext instance, which is the
Entity Framework DbContext class.
public class ProductRepository
{
private MyAppContext _context;
public ProductRepository(MyAppContext context)
{
_context = context;
}
public IEnumerable<Product> GetProductsByTagName(string tagName)
{
var products = _context.Products
.Where(x => x.ProductTags.Any(pt => pt.Tag.Name == tagName))
.ToList();
return products;
}
}
I also have a simple Unit Test that calls this method and tests that the correct Product
is returned. In the test below, the _context variable is an instance of MyAppContext.
Creating this variable for the two different test approaches is explained in the next section of the article.
[TestMethod]
public void GetProductsByTagName_Should_ReturnProduct()
{
var productRepository = new ProductRepository(_context);
IEnumerable<Product> products = productRepository.GetProductsByTagName("Tag 1");
Assert.AreEqual(1, products.Count());
Assert.AreEqual("Product 1", products.First().Name);
}
Testing with Effort
The key to testing will Effort is that we use Effort to create a DbConnection object, which we then use
to initialise our MyAppContext class:
DbConnection connection = Effort.DbConnectionFactory.CreateTransient();
var dbContext = new MyAppContext(connection);
Once we have the DbConnection instance, we can use it again and again to create new DbContext instances,
which effectively simulate different connections to the same database. This is useful as we can create one connection to populate our database
with test data, then create another fresh one to test with.
The DbConnection object is effectively the instance of the database, so if you create a new instance using DbConnectionFactory.CreateTransient(),
it will not contain any data that you have added to a different instance. This means you need to hold onto the instance of the DbConnection object
for the duration of your test.
The following code shows the Test Initialize method that runs before each test. It creates a new DbConnection object representing a new instance of
our database; uses a new DbContext object to add test data to our Entity Mode;, then creates a new DbContext object for use in our test using the same
instance of the DbConnection.
private MyAppContext _context;
[TestInitialize]
public void SetupTest()
{
DbConnection connection = Effort.DbConnectionFactory.CreateTransient();
using (var context = new MyAppContext(connection))
{
context.Products.Add(new Product() { Id = ... });
}
_context = new MyAppContext(connection);
}
When setting up our test data, it's worth mentioning that there's no need to initialize properties that point to
related entites (e.g. Product.ProductTags and Tag.ProductTags). All we need to do is add
individual entities and Effort will automatically associate these using the ID values, just as a real database would.
using (MyAppContext context = new MyAppContext(connection))
{
context.Products.Add(new Product() { Id = new Guid("CEA4655C-..."), Name = "Product 1", ...
context.Products.Add(new Product() { Id = new Guid("A4A989A4-..."), Name = "Product 2", ...
context.Tags.Add(new Tag() { Id = new Guid("D7FE98A2-..."), Name = "Tag 1", ...
context.Tags.Add(new Tag() { Id = new Guid("52FEDB17-..."), Name = "Tag 2", ...
context.Tags.Add(new Tag() { Id = new Guid("45312740-..."), Name = "Tag 3", ...
context.ProductTags.Add(new ProductTag()
{
ProductId = new Guid("CEA4655C-..."),
TagId = new Guid("D7FE98A2-...")
...
});
context.ProductTags.Add(new ProductTag()
{
ProductId = new Guid("CEA4655C-..."),
TagId = new Guid("45312740-...")
...
});
context.ProductTags.Add(new ProductTag()
{
ProductId = new Guid("A4A989A4-..."),
TagId = new Guid("52FEDB17-...")
...
});
}
Testing with SQL CE
When testing with SQL CE, we need to create real SQL CE databases for each test, then initialise our MyAppContext instance with a connection
to this database. The only way I managed to get this to work is by setting the static DefaultConnectionFactory property of the
System.Data.Entity.Database class first, then calling the CreateDatabase() method to generate the database from the Entity Model.
The following implementation of the Test Initialize method can be used to setup our MyAppContext for use in our tests.
[TestInitialize]
public void SetupTest()
{
var filePath = @"C:\code\TestingEf\TestTemp\RealMyAppDb.sdf";
if (File.Exists(filePath))
File.Delete(filePath);
string connectionString = "Datasource = "+filePath;
Database.DefaultConnectionFactory =
new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0");
using (var context = new MyAppContext(connectionString))
{
context.Database.Create();
}
_context = new MyAppContext(connectionString);
}
Swapping between the two methods
Chances are, you might want to use Effort for some tests and SQL CE for others. At first, swapping between the two implementations caused problems: Once the SQL CE method was used, subsequent
uses of the Effort method in the same test run no longer worked.
The problem:
The call to the static DefaultConnectionFactory property to set its value to the SqlCeConnectionFactory
seemed to remove the registration of the Effort provider.
The solution:
Add the following to the App.config of the test project. It adds a registration of the Effort Entity Framework Provider
in addition to the one that the Effort library adds itself at runtime. The name and invariant values are different
to the values that Effort automatically registers at runtime. This has to be the case to avoid conflicts.
<system.data>
<DbProviderFactories>
<add name="Effort Provider Test"
description="Effort Provider for unit testing"
invariant="EffortProviderTest"
type="Effort.Provider.EffortProviderFactory,Effort"/>
</DbProviderFactories>
</system.data>
Downloadable Sample project
Take a look at the sample project for working examples. I've wrapped up the two testing approaches into two different classes:
- TestingEf.Data.Tests.TestDatabaseStrategies.EffortDatabaseStrategy
- TestingEf.Data.Tests.TestDatabaseStrategies.SqlCeDatabaseStrategy
These classes are referenced from the tests in the TestingEf.Data.Tests.ProductRepositoryTests namespace which test
the ProductRepository in more depth than covered in this article. Within these tests, there is a failing test
that highlights how Effort does not support String.IsNullOrEmpty within Entity Framework queries when SQL CE (and SQL Server) does.
The sample project also contains an IMyAppContext interface, which is reference throughout the code instead of using
the concrete MyAppContext implementation directly. Use of the interface is not necessary to explain the testing methods
in this article, but it's good practice for other components of the system to use interfaces as this helps with mocking when testing
different components of the system.
This interface is automatically generated from the MyAppContext.tt T4 template. This is a modified version of the
EF 5 DbContext Generator T4 which generates the classes of the Entity Model as a DbContext class and
POCO objects from an EDMX file. More information about using the DbContext generator can be found here:
http://msdn.microsoft.com/en-US/data/jj206878