ASP.NET MVC Tip #21 – Fake the Data Context

In this tip, I demonstrate how to create an in-memory data context class that you can use when unit testing ASP.NET MVC applications that access a database.

In this tip, I explain how you can write unit tests for data access code within an ASP.NET MVC application. I demonstrate how to unit test the LINQ to SQL DataContext without using a Mock Object Framework. First, I show you how to create a generic DataContextRepository that can be used to retrieve and modify database records. Next, I show you how to create a FakeDataContextRepository that you can use within your unit tests to test your data access code.

The Motivation

If you practice Test-Driven Development, then you don’t write any application code until after you have a test for the code. The tests express your intentions for how you want your code to behave. You always write your code against a test. The tests also provide a safety net that enables you to safely modify your code.

Database access code presents a special problem when practicing Test-Driven Development. The problem is that accessing a database is almost always a slow operation. Since you need to execute your unit tests each and every time that you modify your code, slow running unit tests are a bad idea. Slow tests are bad because they tempt you into abandoning Test-Driven Development as just too time consuming.

How do you respond to this problem of testing your data access code? One response is to avoid testing any code in your application that touches a database. This seems like a very bad option. If Test-Driven Development is so important for good application design, and database access code is such an important part of your application, then you had better figure out some way to get the two to work together.

The second option is to embrace the slowness and unit test your data access code against a test database. The idea is that you generate a new test database right before each of your unit tests is run. This was the option that I explored in MVC Tip #20:

http://weblogs.asp.net/stephenwalther/archive/2008/07/15/asp-net-mvc-tip-20-how-to-unit-test-data-access.aspx

This approach is popular among Ruby on Rails developers. And, it works fine as long as your application remains simple. However, at some point, it just takes too long to run the unit tests and it makes sense to explore the third and final option.

The third and final option is to fake the database. Within your unit tests, instead of accessing a real database, you access a stand-in for the real database. This is the option that I explore in today’s tip. I demonstrate how you can fake the LINQ to SQL DataContext with a simple in-memory database.

Unit Testing Data Access Code

The code download at the end of this tip includes two classes named DataContextRepository and FakeDataContextRepository. Let me demonstrate how you can use these classes by walking through the process of building a simple database-driven MVC application.

Imagine that you decide to build a Movie Database application (see Figure 1). The application needs to support displaying movies, creating new movies, editing movies, and deleting movies (all the basic CRUD). How do you start building this application?

Figure 1 -- The Movie Database Application

image 

If you follow good Test-Driven Development practices, then everything starts with a test. Let’s start by writing a test that verifies whether or not our application returns a list of movies from its Index() method. The test in Listing 1 uses the FakeDataRepository class to test the Index() method.

Listing 1 – HomeControllerTests.vb (VB.NET)

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Web.Mvc
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Tip21
Imports MvcData
Imports System.Collections.Specialized
 
<TestClass()> Public Class HomeControllerTest
 
    Private Function CreateTestMovie(ByVal title As String, ByVal director As String) As Movie
        Dim newMovie As New Movie()
        newMovie.Title = title
        newMovie.Director = director
        Return newMovie
    End Function
 
    <TestMethod()> _
    Public Sub Index()
        ' Setup
        Dim fakeRepository = New FakeDataContextRepository(Of Movie)()
        fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"))
        fakeRepository.Insert(CreateTestMovie("Batman", "Burton"))
 
        Dim controller As New HomeController(fakeRepository)
 
        ' Execute
        Dim result As ViewResult = TryCast(controller.Index(), ViewResult)
 
        ' Verify
        Dim movies = CType(result.ViewData.Model, IList(Of Movie))
        Assert.AreEqual(2, movies.Count)
        Assert.AreEqual("Star Wars", movies(0).Title)
        Assert.AreEqual("Batman", movies(1).Title)
    End Sub
 
 
 
End Class
 

Listing 1 – HomeControllerTests.cs (C#)

using System.Collections.Specialized;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcData;
using Tip21.Controllers;
using Tip21.Models;
using System.Collections.Generic;
 
namespace Tip21Tests.Controllers
{
    [TestClass]
    public class HomeControllerTest
    {
        private Movie CreateTestMovie(string title, string director)
        {
            Movie newMovie = new Movie();
            newMovie.Title = title;
            newMovie.Director = director;
            return newMovie;
        }
 
        [TestMethod]
        public void Index()
        {
            // Setup
            var fakeRepository = new FakeDataContextRepository<Movie>();
            fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"));
            fakeRepository.Insert(CreateTestMovie("Batman", "Burton"));
         
            HomeController controller = new HomeController(fakeRepository);
 
            // Execute
            ViewResult result = controller.Index() as ViewResult;
 
            // Verify
            var movies = (IList<Movie>)result.ViewData.Model;
            Assert.AreEqual(2, movies.Count );
            Assert.AreEqual("Star Wars", movies[0].Title);
            Assert.AreEqual("Batman", movies[1].Title); 
        }
 
     
    }
}
 

The unit test in Listing 1 verifies whether or not the Index() method correctly retrieves database records from the database.

The test class in Listing 1 contains two methods. The first method is a utility method named CreateTestMovie() that enables you to quickly create a test movie. The second method, named Index(), is the unit test for the HomeController.Index() action.

In the Index() method, an instance of the FakeDataContextRepository class is created. Notice that this is a generic class. When instantiating the class, you must provide it with the type of entity that the FakeDataContextRepository will be used to represent. In this case, the FakeDataContextRepository is used to represent Movie entities (The Movie entity is a LINQ to SQL entity that was created with the Visual Studio Object Relational Designer in the application project).

Next, two Movie records are added to the FakeDataContext. After the HomeController.Index() method is called, the test checks whether these records are returned by the Index() method. The test checks the number of records returned and whether or not the title of the first record is “Star Wars” and the second record is “Batman”.

Now that we have a proper unit test, we can write the application code that satisfies the test. The HomeController class is contained in Listing 2.

Listing 2 – HomeController.vb (VB.NET)

Imports MvcData
 
<HandleError()> _
Public Class HomeController
    Inherits Controller
 
    Private _repository As IDataContextRepository(Of Movie)
 
    Public Sub New()
        Me.New(New DataContextRepository(Of Movie)(New MovieDataContext()))
    End Sub
 
    Public Sub New(ByVal repository As IDataContextRepository(Of Movie))
        _repository = repository
    End Sub
 
    Public Function Index() As ActionResult
        Dim movies As IList(Of Movie) = _repository.ListAll()
        Return View(movies)
    End Function
 
End Class

Listing 2 – HomeController.cs (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcData;
using Tip21.Models;
using System.Collections.Specialized;
 
namespace Tip21.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        private IDataContextRepository<Movie> _repository;
 
        public HomeController() : this(new DataContextRepository<Movie>(new MovieDataContext())) { }
 
        public HomeController(IDataContextRepository<Movie> repository)
        {
            _repository = repository;
        }
        
        public ActionResult Index()
        {
            IList<Movie> movies = _repository.ListAll();
            return View(movies);
        }
 
   
    }
}
 

The HomeController class in Listing 2 contains two constructors. The first constructor, a parameterless constructor, creates a new instance of the DataContextRepository class and passes it to the second constructor. The second constructor assigns the DataContextRepository to a class field. The first constructor is called when the Movie application is actually running. The second constructor is called by our unit tests (This is an example of Dependency Injection).

Notice that the HomeController class – except in the case of the first parameterless constructor – never refers to the DataContextRepository class. Instead, the class uses the IDataContextRepository interface. Using an interface instead of the concrete class is important because it enables us to substitute the fake DataContext in place of the real DataContext.

The Index() action calls the ListAll() method of the DataContextRepository to get a list of movies. This list is passed to the Index view (This view is strongly typed to accept an IList of Movie objects).

The Index view is contained in Listing 3. This view simply iterates through the list of movies and displays the movie titles in a bulleted list.

Listing 3 – Index.aspx (VB)

<%@ Page Language="VB" AutoEventWireup="false" CodeBehind="Index.aspx.vb" Inherits="Tip21.Index" %>
<%@ Import Namespace="Tip21" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <div>
 
    <ul>
    <%  For Each movie As Movie In ViewData.Model%>
    
        <li>
            <%=Html.ActionLink("Edit", "Edit", New With {.id = movie.Id})%>
            <a href="/Home/Delete/<%= movie.Id %>" onclick="return confirm('Delete <%=movie.Title %>?')">Delete</a>
            &nbsp;
            <%= movie.Title %>
        </li>
    <% Next%>
    </ul>
    <%= Html.ActionLink("Add Movie", "Create") %>
 
    
    </div>
</body>
</html>
 

Listing 3 – Index.aspx (C#)

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="Tip21.Views.Home.Index" %>
<%@ Import Namespace="Tip21.Models" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
</head>
<body>
    <div>
    <ul>
    <% foreach (Movie movie in ViewData.Model)
       { %>
    
        <li>
            <%= Html.ActionLink("Edit", "Edit", new {id=movie.Id}) %>
            <a href="/Home/Delete/<%= movie.Id %>" onclick="return confirm('Delete <%=movie.Title %>?')">Delete</a>
            &nbsp;
            <%= movie.Title %>
        </li>
    <% } %>
    </ul>
    <%= Html.ActionLink("Add Movie", "Create") %>
    
    </div>
</body>
</html>

This was a very straightforward process. We created the unit test for the Index() method with the help of the FakeDataContextRepository. Next, we satisfied the unit test with our real DataContextRepository class. Have we done anything illegal here? Will the TDD police put us in jail?

Just as long as the behavior of the FakeDataContextRepository class is the same as the DataContextRepository class, we should be fine. The FakeDataContextRepository class provides us with a fast way to unit test our data access code.

Let’s look at a couple of other cases of unit testing database access code. In Listing 4, I’ve modified the HomeControllerTest class so that it includes tests for the Index(), Insert(), Update(), and Delete() methods.

Listing 4 – HomeControllerTest.vb (VB.NET)

Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Web.Mvc
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Tip21
Imports MvcData
Imports System.Collections.Specialized
 
<TestClass()> Public Class HomeControllerTest
 
    Private Function CreateTestMovie(ByVal title As String, ByVal director As String) As Movie
        Dim newMovie As New Movie()
        newMovie.Title = title
        newMovie.Director = director
        Return newMovie
    End Function
 
    <TestMethod()> _
    Public Sub Index()
        ' Setup
        Dim fakeRepository = New FakeDataContextRepository(Of Movie)()
        fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"))
        fakeRepository.Insert(CreateTestMovie("Batman", "Burton"))
 
        Dim controller As New HomeController(fakeRepository)
 
        ' Execute
        Dim result As ViewResult = TryCast(controller.Index(), ViewResult)
 
        ' Verify
        Dim movies = CType(result.ViewData.Model, IList(Of Movie))
        Assert.AreEqual(2, movies.Count)
        Assert.AreEqual("Star Wars", movies(0).Title)
        Assert.AreEqual("Batman", movies(1).Title)
    End Sub
 
    <TestMethod()> _
    Public Sub Insert()
        ' Setup
        Dim fakeRepository = New FakeDataContextRepository(Of Movie)()
        Dim controller As New HomeController(fakeRepository)
 
        Dim formParams As New NameValueCollection()
        formParams.Add("title", "Star Wars")
        formParams.Add("Director", "Lucas")
 
        ' Execute
        controller.Insert(formParams)
 
        ' Verify
        Dim movies = fakeRepository.ListAll()
        Assert.AreEqual(1, movies.Count)
        Assert.AreEqual("Star Wars", movies(0).Title)
    End Sub
 
 
    <TestMethod()> _
    Public Sub Update()
        ' Setup
        Dim fakeRepository = New FakeDataContextRepository(Of Movie)()
        Dim newMovie As Movie = fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"))
        Dim controller As New HomeController(fakeRepository)
 
        Dim formParams As New NameValueCollection()
        formParams.Add("id", newMovie.Id.ToString())
        formParams.Add("title", "Star Wars Changed")
        formParams.Add("Director", "Lucas Changed")
 
        ' Execute
        controller.Update(formParams)
 
        ' Verify
        Dim movies = fakeRepository.ListAll()
        Assert.AreEqual(1, movies.Count)
        Assert.AreEqual("Star Wars Changed", movies(0).Title)
    End Sub
 
    <TestMethod()> _
    Public Sub Delete()
        ' Setup
        Dim fakeRepository = New FakeDataContextRepository(Of Movie)()
        Dim movieToDelete As Movie = fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"))
        Dim controller As New HomeController(fakeRepository)
 
        Dim formParams As New NameValueCollection()
        formParams.Add("id", movieToDelete.Id.ToString())
 
        ' Execute
        controller.Delete(movieToDelete.Id)
 
        ' Verify
        Dim movies = fakeRepository.ListAll()
        Assert.AreEqual(0, movies.Count)
    End Sub
 
 
End Class

Listing 4 – HomeControllerTest.cs (C#)

using System.Collections.Specialized;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcData;
using Tip21.Controllers;
using Tip21.Models;
using System.Collections.Generic;
 
namespace Tip21Tests.Controllers
{
    [TestClass]
    public class HomeControllerTest
    {
        private Movie CreateTestMovie(string title, string director)
        {
            Movie newMovie = new Movie();
            newMovie.Title = title;
            newMovie.Director = director;
            return newMovie;
        }
 
        [TestMethod]
        public void Index()
        {
            // Setup
            var fakeRepository = new FakeDataContextRepository<Movie>();
            fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"));
            fakeRepository.Insert(CreateTestMovie("Batman", "Burton"));
         
            HomeController controller = new HomeController(fakeRepository);
 
            // Execute
            ViewResult result = controller.Index() as ViewResult;
 
            // Verify
            var movies = (IList<Movie>)result.ViewData.Model;
            Assert.AreEqual(2, movies.Count );
            Assert.AreEqual("Star Wars", movies[0].Title);
            Assert.AreEqual("Batman", movies[1].Title); 
        }
 
        [TestMethod]
        public void Insert()
        {
            // Setup
            var fakeRepository = new FakeDataContextRepository<Movie>();
            HomeController controller = new HomeController(fakeRepository);
 
            NameValueCollection formParams = new NameValueCollection();
            formParams.Add("title", "Star Wars");
            formParams.Add("Director", "Lucas");
 
            // Execute
            controller.Insert(formParams);
 
            // Verify
            var movies = fakeRepository.ListAll();
            Assert.AreEqual(1, movies.Count);
            Assert.AreEqual("Star Wars", movies[0].Title);
        }
 
 
        [TestMethod]
        public void Update()
        {
            // Setup
            var fakeRepository = new FakeDataContextRepository<Movie>();
            Movie newMovie = fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"));            
            HomeController controller = new HomeController(fakeRepository);
 
            NameValueCollection formParams = new NameValueCollection();
            formParams.Add("id", newMovie.Id.ToString());
            formParams.Add("title", "Star Wars Changed");
            formParams.Add("Director", "Lucas Changed");
 
            // Execute
            controller.Update(formParams);
 
            // Verify
            var movies = fakeRepository.ListAll();
            Assert.AreEqual(1, movies.Count);
            Assert.AreEqual("Star Wars Changed", movies[0].Title);
        }
 
        [TestMethod]
        public void Delete()
        {
            // Setup
            var fakeRepository = new FakeDataContextRepository<Movie>();
            Movie movieToDelete = fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"));
            HomeController controller = new HomeController(fakeRepository);
 
            NameValueCollection formParams = new NameValueCollection();
            formParams.Add("id", movieToDelete.Id.ToString());
 
            // Execute
            controller.Delete(movieToDelete.Id);
 
            // Verify
            var movies = fakeRepository.ListAll();
            Assert.AreEqual(0, movies.Count);
        }
 
 
 
      
    }
}

Here’s how the Insert() test works. First, the FakeDataContextRepository is passed to the HomeController. Next, the form fields passed from the HTML form are faked with a NameValueCollection and passed to the HomeController.Insert() method. Finally, the FakeDataContextRepository is used to verify whether or not the new record was actually inserted into the database.

The complete code for the HomeController class is contained in Listing 5.

Listing 5 – HomeController.vb (VB.NET)

Imports MvcData
 
 
 
<HandleError()> _
Public Class HomeController
    Inherits Controller
 
    Private _repository As IDataContextRepository(Of Movie)
 
    Public Sub New()
        Me.New(New DataContextRepository(Of Movie)(New MovieDataContext()))
    End Sub
 
    Public Sub New(ByVal repository As IDataContextRepository(Of Movie))
        _repository = repository
    End Sub
 
    Public Function Index() As ActionResult
        Dim movies As IList(Of Movie) = _repository.ListAll()
        Return View(movies)
    End Function
 
    Public Function Create() As ActionResult
        Return View()
    End Function
 
    Public Function Insert(ByVal formParams As NameValueCollection) As ActionResult
        _repository.Insert(formParams)
        Return RedirectToAction("Index")
    End Function
 
    Public Function Edit(ByVal id As Integer) As ActionResult
        Return View(_repository.Get(id))
    End Function
 
    Public Function Update(ByVal formParams As NameValueCollection) As ActionResult
        _repository.Update(formParams)
        Return RedirectToAction("Index")
    End Function
 
    Public Function Delete(ByVal id As Integer) As ActionResult
        _repository.Delete(id)
        Return RedirectToAction("Index")
    End Function
 
End Class
 
 

Listing 5 – HomeController.cs (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcData;
using Tip21.Models;
using System.Collections.Specialized;
 
namespace Tip21.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        private IDataContextRepository<Movie> _repository;
 
        public HomeController() : this(new DataContextRepository<Movie>(new MovieDataContext())) { }
 
        public HomeController(IDataContextRepository<Movie> repository)
        {
            _repository = repository;
        }
        
        public ActionResult Index()
        {
            IList<Movie> movies = _repository.ListAll();
            return View(movies);
        }
 
        public ActionResult Create()
        {
            return View();
        }
 
        public ActionResult Insert(NameValueCollection formParams)
        {
            _repository.Insert(formParams);
            return RedirectToAction("Index");
        }
 
        public ActionResult Edit(int id)
        {
            return View(_repository.Get(id));
        }
 
        public ActionResult Update(NameValueCollection formParams)
        {
            _repository.Update(formParams);
            return RedirectToAction("Index");
        }
 
        public ActionResult Delete(int id)
        {
            _repository.Delete(id);
            return RedirectToAction("Index");
        }
    
    }
}

One special thing that I should warn you about. Notice that the Insert() and Update() actions accept a NameValueCollection named formParams that represent all of the HTML form fields submitted to the actions. To get the magic formParams parameter to work, I am taking advantage of a custom Action Invoker. See ASP.NET MVC Tip #18:

http://weblogs.asp.net/stephenwalther/archive/2008/07/11/asp-net-mvc-tip-18-parameterize-the-http-context.aspx

Extending the DataContext Repository

At some point, you will need to modify the DataContextRepository class so that it supports more data access methods. For example, imagine that you decide to create a method named GetFirstMovie() that returns the last movie added to the database. If you discover that you need to create new data access methods, then I recommend that you inherit a new class from both the DataContextRepository and FakeDataContextRepository classes and create a new interface.

The code in Listing 6 includes the three new classes.

Listing 6 – MovieRepository.vb (VB.NET)

Imports MvcData
Imports System.Data.Linq
 
Public Interface IMovieRepository
    Inherits IDataContextRepository(Of Movie)
 
    Function GetFirstMovie() As Movie
 
End Interface
 
Public Class MovieRepository
    Inherits DataContextRepository(Of Movie)
    Implements IMovieRepository
 
    Public Function GetFirstMovie() As Movie Implements IMovieRepository.GetFirstMovie
        Return Me.Table.First()
    End Function
 
    Public Sub New(ByVal connectionString As String)
        MyBase.New(connectionString)
    End Sub
 
    Public Sub New(ByVal dataContext As DataContext)
        MyBase.New(dataContext)
    End Sub
End Class
 
Public Class FakeMovieRepository
    Inherits FakeDataContextRepository(Of Movie)
    Implements IMovieRepository
 
    Public Function GetFirstMovie() As Movie Implements IMovieRepository.GetFirstMovie
        Return Me.FakeTable.Values.First()
    End Function
End Class

Listing 6 – MovieRepository.cs (C#)

using System.Data.Linq;
using System.Linq;
using MvcData;
 
namespace Tip21.Models
{
    public interface IMovieRepository : IDataContextRepository<Movie>
    {
        Movie GetFirstMovie();
    }
 
    public class MovieRepository : DataContextRepository<Movie>, IMovieRepository
    {
        public Movie GetFirstMovie()
        {
            return this.Table.First();
        }
 
        public MovieRepository(string connectionString ) : base(connectionString) {}
 
        public MovieRepository(DataContext dataContext ) : base(dataContext) {}
    }
 
    public class FakeMovieRepository : FakeDataContextRepository<Movie>, IMovieRepository
    {
        public Movie GetFirstMovie()
        {
            return this.FakeTable.Values.First();
        }
    }
}

The code in Listing 7 illustrates how you can use the MovieRepository with a new controller named MovieController.

Listing 7 – MovieController.vb (VB.NET)

Imports MvcData
 
Public Class MovieController
    Inherits Controller
 
    Private _repository As IMovieRepository
 
    Public Sub New()
        Me.New(New MovieRepository(New MovieDataContext()))
    End Sub
 
    Public Sub New(ByVal repository As IMovieRepository)
        _repository = repository
    End Sub
 
    Public Function Index() As ActionResult
        Dim firstMovie = _repository.GetFirstMovie()
        Return View(firstMovie)
    End Function
End Class

Listing 7 – MovieController.cs (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Tip21.Models;
 
namespace Tip21.Controllers
{
    public class MovieController : Controller
    {
        private IMovieRepository _repository;
 
        public MovieController() : this(new MovieRepository(new MovieDataContext())) { }
 
        public MovieController(IMovieRepository repository)
        {
            _repository = repository;
        }
        
        public ActionResult Index()
        {
            var firstMovie = _repository.GetFirstMovie();
            return View(firstMovie);
        }
    }
}

Finally, the MovieControllerTest class in Listing 8 illustrates how you can use the FakeMovieRepository to test the MovieController class.

Listing 8 – MovieControllerTest.vb (vb.net)

Imports System
Imports System.Text
Imports System.Collections.Generic
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports MvcData
Imports Tip21
Imports System.Web.Mvc
 
<TestClass()> Public Class MovieControllerTest
 
    Private Function CreateTestMovie(ByVal title As String, ByVal director As String) As Movie
        Dim newMovie As New Movie()
        newMovie.Title = title
        newMovie.Director = director
        Return newMovie
    End Function
 
    <TestMethod()> _
    Public Sub Index()
        ' Setup
        Dim fakeRepository = New FakeMovieRepository()
        fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"))
        fakeRepository.Insert(CreateTestMovie("Batman", "Burton"))
 
        Dim controller As New MovieController(fakeRepository)
 
        ' Execute
        Dim result As ViewResult = TryCast(controller.Index(), ViewResult)
 
        ' Verify
        Dim model = CType(result.ViewData.Model, Movie)
        Assert.AreEqual("Star Wars", model.Title)
    End Sub
 
End Class

Listing 8 – MovieControllerTest.cs (C#)

using System.Collections.Specialized;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcData;
using Tip21.Controllers;
using Tip21.Models;
 
namespace Tip21Tests.Controllers
{
    [TestClass]
    public class MovieControllerTest
    {
 
        private Movie CreateTestMovie(string title, string director)
        {
            Movie newMovie = new Movie();
            newMovie.Title = title;
            newMovie.Director = director;
            return newMovie;
        }
 
        [TestMethod]
        public void Index()
        {
            // Setup
            var fakeRepository = new FakeMovieRepository();
            fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"));
            fakeRepository.Insert(CreateTestMovie("Batman", "Burton"));
 
            MovieController controller = new MovieController(fakeRepository);
 
            // Execute
            ViewResult result = controller.Index() as ViewResult;
 
            // Verify
            var model = (Movie)result.ViewData.Model;
            Assert.AreEqual("Star Wars", model.Title);
        }
 
 
    }
}

The unit test in Listing 8 uses the FakeMovieRepository class to fake a repository with two movies. Next, the MovieController.Index() method is called. If the movie returned by the Index() method corresponds to the first movie in the fake repository then success is achieved. Otherwise, you know the MovieController.Index() method is broken.

A Moment of Reflection

You might ask whether or not it is really useful to build unit tests for a fake object instead of the real object. Aren’t we just testing whether or not the fake object works? Aren’t we avoiding testing the real object?

The answer to this question is yes and no. It is true that the MovieRepository.GetFirstMovie() method never gets tested. Instead, only the FakeMovieRepository.GetFirstMovie() method gets tested and we don’t really care whether or not that method works. So, faking is a form of test avoidance.

However, the goal is to fake as little as possible. When we call the MovieController.Index() method from our unit tests, we test all of the logic that surrounds the FakeMovieRepository class. In our toy MovieController class, there really isn’t any business logic to test. But, in a real application, controllers interact with models that often contain painfully complex business logic.

If you are careful not to place any of your business logic in your repository, then faking a repository is useful. Faking a repository is useful since it enables you to test all of the application logic that interacts with the repository.

This point is worth emphasizing more strongly: Don’t put your business logic in your repository. If you do, then you are just hiding the business logic from your unit tests.

If you really want to test the MovieRepository class itself, and not the fake version, then you should test the MovieRepository class in an integration test. You don’t run integration tests with each and every code modification. Instead, you might run your integration tests only once or twice a day.

Summary

In this tip, I’ve explored one approach to unit testing your data access code within the context of Test-Driven Development. I’ve demonstrated how you can create a FakeDataContextRepository class that you can use in your unit tests. I’ve also shown you how to extend the base DataContextRepository and FakeDataContextRepository class to create new data access methods.

With this tip and the previous tip, I’ve provided you with two options for unit testing data access code. In ASP.NET MVC Tip #20, I demonstrated how you can generate a test database automatically. In this tip, I demonstrated how to fake the database.

 

Download the Code

3 Comments

  • Maybe I missed something, but why making a fake database repository instead of using a level of indirection between the controller and the datacontext and then use some mocking framework for the tests?

  • Hi,
    Nice post, but there are a few smells (IMHO).

    Your views effectively contain business logic in their call to repository.ListAll - ideally they would expect IEnumerable, or IList as their model.
    Your first test basically tests that the repository passed to the controller ends up passed to the view. i.e. Asert.AreSame(fakeRepository, result.ViewData.Model) would acheive the same result.

    Changing your test to:
    [TestMethod]
    public void Index()
    {
    var fakeRepository = new FakeDataContextRepository();
    fakeRepository.Insert(CreateTestMovie("Star Wars", "Lucas"));
    fakeRepository.Insert(CreateTestMovie("Batman", "Burton"));
    HomeController controller = new HomeController(fakeRepository);

    ViewResult result = controller.Index() as ViewResult;

    var movies = (IList)result.ViewData.Model;
    Assert.AreEqual(2, movies.Count );
    Assert.AreEqual("Star Wars", movies[0].Title);
    Assert.AreEqual("Batman", movies[1].Title);
    }

  • @Joshka -- Good points (I agree with both). I updated the code to be less smelly.

Comments have been disabled for this content.