Practical .NET
Test-Driven Development with Entity Framework 6
You can completely disconnect your test code from your database with Entity Framework 6 in .NET and Visual Studio. You just need a little bit of custom code for your application, along with some boilerplate code.
I'm a big fan of Test-Driven Development (TDD), but I recognize the issue that any reasonable business developer has with it: Testing the code that works with the database. Entity Framework (EF) provides part of the solution by letting you instantiate the EF classes that represent the tables in your database to use in your tests. This strategy also lets you instantiate entity objects targeted for specific tests -- generating a SalesOrder object that's missing specific information, or one that has no matching Customer object, or one that's in a specific status.
EF6 also lets you create a mock DbContext object that holds your mock entity objects. All you need is a little custom code that, combined with some boilerplate code (code that you can use unchanged in multiple projects), gives you a context object completely detached from your database. The major restriction in using a mock context object is in the way you write your data access methods: Any method that uses the context object can't be responsible for creating it. Your data access methods must be passed a context object holding your entities. Sometimes your method will be passed a "real" context object and sometimes it will be passed your mock context object.
Creating the Mock Context
The first set of custom code you need is an interface to be used with both your "real" and your mock context object. This interface ensures your application (and your test code) can't tell the difference between your real and mock DbContext objects. Your interface needs to include a DbSet property for each entity class your application uses. The interface must also include the context object's SaveChanges method:
Public Interface ISalesOrderContext
Property Orders As DbSet(Of SalesOrder)
Property Customers As DbSet(Of Customer)
Function SaveChanges() As Integer
End Interface
You now have both your "real" context object and your mock context object, both of which implement this interface. Your real context object looks like this (Visual Studio will write most of this code once you type in the Implements line):
Public Class SalesOrderContext
Inherits DbContext
Implements ISalesOrderContext
Public Property Orders As DbSet(Of SalesOrder) Implements ISalesOrderContext.Orders
Public Property Customers As DbSet(Of Customer) Implements ISalesOrderContext.Customers
Public Function SaveChangesInterface() As Integer Implements ISalesOrderContext.SaveChanges
Return MyBase.SaveChanges
End Function
End Class
Your mock context object also implements the ISalesOrderContext interface (once again letting Visual Studio write most of the code). The major difference between your mock context object and your real context object is that your mock context object doesn't inherit from DbContext:
Public Class SalesOrderMockContext
Implements ISalesOrderContext
Public Property Orders As DbSet(Of SalesOrder) Implements ISalesOrderContext.Orders
Public Property Customers As DbSet(Of Customer) Implements ISalesOrderContext.Customers
Public Function SaveChanges() As Integer Implements ISalesOrderContext.SaveChanges
Return 0
End Function
Your mock context object also needs a constructor that loads a mock DbSet class into your DbSet properties. Something like this does the job:
Public Sub New()
Orders = New TestDbSet(Of SalesOrder)
Customers = New TestDbSet(Of Customer)
End Sub
Boilerplate Code
That's all the custom code you'll need to write for each application. You also need, though, the TestDbSet class I referenced in my mock context's constructor. That TestDbSet class, in turn, needs some supporting classes. The good news is that these classes are the boilerplate code I mentioned and that code's available to be copied from the Microsoft Data Developer Center. The bad news is the code is only available in C#. I've included the Visual Basic code in Listing 1. Just copy this code into a file and compile your tests. And make sure you put those files someplace where you can recycle them into other projects.
Listing 1: Visual Basic Boilerplate Code for a Mock DbSet
Public Class TestDbSet(Of TEntity As Class)
Inherits DbSet(Of TEntity)
Implements IQueryable
Implements IEnumerable(Of TEntity)
Implements IDbAsyncEnumerable(Of TEntity)
Private _data As ObservableCollection(Of TEntity)
Private _query As IQueryable
Public Sub New()
_data = New ObservableCollection(Of TEntity)()
_query = _data.AsQueryable()
End Sub
Public Overrides Function Add(item As TEntity) As TEntity
_data.Add(item)
Return item
End Function
Public Overrides Function Remove(item As TEntity) As TEntity
_data.Remove(item)
Return item
End Function
Public Overrides Function Attach(item As TEntity) As TEntity
_data.Add(item)
Return item
End Function
Public Overrides Function Create() As TEntity
Return Activator.CreateInstance(Of TEntity)()
End Function
Public Overrides Function Create(Of TDerivedEntity As {Class, TEntity})() As TDerivedEntity
Return MyBase.Create(Of TDerivedEntity)()
End Function
Public Overrides ReadOnly Property Local() As ObservableCollection(Of TEntity)
Get
Return _data
End Get
End Property
Private ReadOnly Property IQueryable_ElementType() As Type Implements IQueryable.ElementType
Get
Return _query.ElementType
End Get
End Property
Private ReadOnly Property IQueryable_Expression()
As Expression Implements IQueryable.Expression
Get
Return _query.Expression
End Get
End Property
Private ReadOnly Property IQueryable_Provider()
As IQueryProvider Implements IQueryable.Provider
Get
Return New TestDbAsyncQueryProvider(Of TEntity)(_query.Provider)
End Get
End Property
Private Function System_Collections_IEnumerable_GetEnumerator()
As System.Collections.IEnumerator Implements System.Collections.IEnumerable.GetEnumerator
Return _data.GetEnumerator()
End Function
Private Function IEnumerable_GetEnumerator()
As IEnumerator(Of TEntity) Implements IEnumerable(Of TEntity).GetEnumerator
Return _data.GetEnumerator()
End Function
Private Function IDbAsyncEnumerable_GetAsyncEnumerator()
As IDbAsyncEnumerator(Of TEntity) Implements IDbAsyncEnumerable(
Of TEntity).GetAsyncEnumerator
Return New TestDbAsyncEnumerator(Of TEntity)(_data.GetEnumerator())
End Function
End Class
Friend Class TestDbAsyncQueryProvider(Of TEntity)
Implements IDbAsyncQueryProvider
Private ReadOnly _inner As IQueryProvider
Friend Sub New(inner As IQueryProvider)
_inner = inner
End Sub
Public Function CreateQuery(expression As Expression)
As IQueryable Implements IDbAsyncQueryProvider.CreateQuery
Return New TestDbAsyncEnumerable(Of TEntity)(expression)
End Function
Public Function CreateQuery(Of TElement)(expression As Expression)
As IQueryable(Of TElement) Implements IDbAsyncQueryProvider.CreateQuery
Return New TestDbAsyncEnumerable(Of TElement)(expression)
End Function
Public Function Execute(expression As Expression)
As Object Implements IDbAsyncQueryProvider.Execute
Return _inner.Execute(expression)
End Function
Public Function Execute(Of TResult)(expression As Expression)
As TResult Implements IQueryProvider.Execute
Return _inner.Execute(Of TResult)(expression)
End Function
Public Function ExecuteAsync(
expression As Expression, cancellationToken As CancellationToken)
As Task(Of Object) Implements IDbAsyncQueryProvider.ExecuteAsync
Return Task.FromResult(Execute(expression))
End Function
Public Function ExecuteAsync(Of TResult)(
expression As Expression, cancellationToken As CancellationToken)
As Task(Of TResult) Implements IDbAsyncQueryProvider.ExecuteAsync
Return Task.FromResult(Execute(Of TResult)(expression))
End Function
End Class
Friend Class TestDbAsyncEnumerable(Of T)
Inherits EnumerableQuery(Of T)
Implements IDbAsyncEnumerable(Of T)
Public Sub New(enumerable As IEnumerable(Of T))
MyBase.New(enumerable)
End Sub
Public Sub New(expression As Expression)
MyBase.New(expression)
End Sub
Public Function GetAsyncEnumerator()
As IDbAsyncEnumerator(Of T) Implements IDbAsyncEnumerable(Of T).GetAsyncEnumerator
Return New TestDbAsyncEnumerator(Of T)(Me.AsEnumerable().GetEnumerator())
End Function
Private Function IDbAsyncEnumerable_GetAsyncEnumerator()
As IDbAsyncEnumerator Implements IDbAsyncEnumerable.GetAsyncEnumerator
Return GetAsyncEnumerator()
End Function
Public ReadOnly Property Provider() As IQueryProvider
Get
Return New TestDbAsyncQueryProvider(Of T)(Me)
End Get
End Property
End Class
Friend Class TestDbAsyncEnumerator(Of T)
Implements IDbAsyncEnumerator(Of T)
Implements IDisposable
Private ReadOnly _inner As IEnumerator(Of T)
Public Sub New(inner As IEnumerator(Of T))
_inner = inner
End Sub
Public Sub Dispose()
_inner.Dispose()
End Sub
Public Function MoveNextAsync(cancellationToken As CancellationToken)
As Task(Of Boolean) Implements IDbAsyncEnumerator(Of T).MoveNextAsync
Return Task.FromResult(_inner.MoveNext())
End Function
Public ReadOnly Property Current() As T Implements IDbAsyncEnumerator(Of T).Current
Get
Return _inner.Current
End Get
End Property
Private ReadOnly Property IDbAsyncEnumerator_Current()
As Object Implements IDbAsyncEnumerator.Current
Get
Return Current
End Get
End Property
Public Sub Dispose1() Implements IDisposable.Dispose
End Sub
End Class
To use this mock context object your project will need, in addition to EF6, a reference to System.Data.Entity. Now, to run a test, just instantiate your mock context object, use its Add method to add some mock entity objects to it, as the code in Listing 2 does.
Listing 2: Instantiate the Mock Context Object Using Its Add Method
Dim mockCtx As SalesOrderMockContext
<TestInitialize>
Public Sub StockContextWithCustomers()
Dim cust As Customer
cust = New Customer With {
.CustomerId = 1,
.FirstName = "Peter",
.LastName = "Vogel"
}
mockCtx.Customers.Add(cust)
cust = New Customer With {
.CustomerId = 2,
.FirstName = "Jan",
.LastName = "Vogel"
}
mockCtx.Customers.Add(cust)
'add more customers, including special cases
'(e.g. customers with missing data)
With your mock context created and populated with some useful test data, you can write tests like this one:
<TestMethod>
Public Sub GetCustomer
Dim res = (From c In mockCtx.Customers
Where c.FirstName = "Peter"
Select c).FirstOrDefault
Assert.IsNotNull(res, "No customer found")
Assert.AreEqual(1, res.CustomerId, "Wrong customer found")
End Sub
More Efficient Integration Testing
I'd love to say that this combination of custom and boilerplate code will give you a complete testing solution. However, LINQ queries that are run against the mock context object are produced by LINQ-to-Objects and not LINQ-to-Entities. At the very least, eager and lazy loading may affect the way your application behaves -- you'll still need to do some integration testing against your actual database. That being said, EF6 support for TDD will let you reduce the number of surprises you'll discover when you do finally get around to that integration testing.
About the Author
Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.