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/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube