ASP.NET MVC Tip #33 – Unit Test LINQ to SQL
In this tip, I demonstrate how to unit test the LINQ to SQL DataContext object by creating a Fake DataContext. You can perform standard LINQ to SQL inserts, updates, deletes and LINQ queries against the Fake DataContext.
I’ve struggled for the past couple of months with different methods of unit testing MVC controllers that return and update database data. I want an easy way of unit testing the database access code in my ASP.NET MVC applications.
I almost gave up until Rob Conery stopped by my office and showed me an easy method of performing LINQ to SQL queries against a standard collection. Once I made it over that hurdle, the rest of the process of building the FakeDataContext class was straightforward (thanks Rob!).
In this tip, I show how you can create a testable LINQ to SQL DataContext object. First, I demonstrate how you can create a wrapper for the standard DataContext object. By creating a DataContext wrapper, you can program against an abstraction instead of a concrete class.
Next, I show how you can fake the DataContext object with almost full fidelity. We create a FakeDataContext that supports inserts, deletes, and standard LINQ to SQL queries (our fake class supports the IQueryable interface).
Wrapping Up the DataContext Class
The first step required to create a testable DataContext object is to create a wrapper class. The standard DataContext object does not implement an interface and it does not derive from a base class. Even worse, it returns sealed Table classes. This means that we can’t swap the DataContext object for a fake DataContext in our unit tests.
The DataContext wrapper class is contained in Listing 1.
Listing 1 – DataContextWrapper.cs
using System.Configuration; using System.Data.Linq; using System.Data.Linq.Mapping; using System.Web.Configuration; using System.Web.Hosting; namespace MvcFakes { public class DataContextWrapper : IDataContext { private DataContext _dataContext; public DataContextWrapper(string connectionStringName, string xmlMapPath) { var conSettings = WebConfigurationManager.ConnectionStrings[connectionStringName]; if (conSettings == null) throw new ConfigurationErrorsException("Missing " + connectionStringName + " connection string in web configuration."); var map = XmlMappingSource.FromUrl(HostingEnvironment.MapPath(xmlMapPath)); _dataContext = new DataContext(conSettings.ConnectionString, map); } public DataContextWrapper(string fileOrServerOrConnection) { _dataContext = new DataContext(fileOrServerOrConnection); } public DataContextWrapper(string fileOrServerOrConnection, MappingSource mapping) { _dataContext = new DataContext(fileOrServerOrConnection, mapping); } public void SubmitChanges() { _dataContext.SubmitChanges(); } public ITable<TEntity> GetTable<TEntity>() where TEntity:class { return new TableWrapper<TEntity>(_dataContext.GetTable<TEntity>()); } } }
The DataContextWrapper class in Listing 1 creates a standard DataContext in its constructor. The class implements the same methods as the standard DataContext object. When you call a method of the DataContextWrapper class, the class delegates the call to the standard DataContext class.
So why wrap? Wrapping the DataContext class enables us to add an interface to the DataContext class.
Notice that the DataContextWrapper implements an interface called the IDataContext interface. This interface is not a standard part of the .NET framework. This interface is contained in Listing 2.
Listing 2 – IDataContext.cs
namespace MvcFakes { public interface IDataContext { void SubmitChanges(); ITable<TEntity> GetTable<TEntity>() where TEntity : class; } }
The interface in Listing 2 has two methods: SubmitChanges() and GetTable(). Both the DataContextWrapper class and the FakeDataContext class implement these two methods.
The pattern used here is the same pattern used for the classes in the System.Web.Abstractions namespace. This namespace contains wrapper classes for the standard ASP.NET intrinsics such as the HtpContext, HttpResponse, and HttpSessionState intrinsics. The wrappers add interfaces (and base classes) to these standard ASP.NET classes.
Creating a Fake DataContext
The code for the Fake DataContext class is a little too long to post here (but you can download it by clicking the link at the end of this blog entry).
The FakeDataContext class also implements the IDataContext interface. The FakeDataContext does not interact with a real database. Instead, the FakeDataContext interacts with data stored in-memory.
You can perform standard LINQ to SQL operations and queries against the FakeDataContext object. For example, the unit test in Listing 3 use the FakeDataContext object. First, the test adds data to the FakeDataContext by using the standard DataContext InsertOnSubmit() method. Next, a query is performed to return all records where the Title property starts with the letter “S”. If two records are returned, then the test is successful.
Listing 3 – TestWhere()
[TestMethod] public void TestWhere() { // Arrange var dataContext = new FakeDataContext(); // Act var table = dataContext.GetTable<Movie>(); table.InsertOnSubmit(new Movie("Lion King", "Disney")); table.InsertOnSubmit(new Movie("King Kong", "Jackson")); table.InsertOnSubmit(new Movie("Star Wars", "Lucas")); table.InsertOnSubmit(new Movie("Superman", "Spelling")); dataContext.SubmitChanges(); // Assert var movies = from m in table where m.Title.StartsWith("S") select m; Assert.AreEqual(2, movies.Count()); }
Using the Fake DataContext
Let’s see how we can use the FakeDataContext object when building a simple Movie database application. The Home controller in Listing 4 contains actions for displaying movies, inserting new movies, and updating existing movies.
Listing 4 – HomeController.cs
using System.Linq; using System.Web.Mvc; using MvcFakes; using Tip33.Models; namespace Tip33.Controllers { public class HomeController : Controller { private IDataContext _dataContext; public HomeController():this(new DataContextWrapper("dbcon", "~/Models/Movie.xml")) {} public HomeController(IDataContext dataContext) { _dataContext = dataContext; } public ActionResult Index() { var table = _dataContext.GetTable<Movie>(); var movies = from m in table select m; return View("Index", movies.ToList()); } public ActionResult Create() { return View("Create"); } public ActionResult Insert(string title, string director) { var newMovie = new Movie(title, director); _dataContext.GetTable<Movie>().InsertOnSubmit(newMovie); _dataContext.SubmitChanges(); return RedirectToAction("Index"); } public ActionResult Edit(int Id) { var table = _dataContext.GetTable<Movie>(); var movie = table.SingleOrDefault(m=>m.Id==Id); return View("Edit", movie); } public ActionResult Update(int id, string title, string director) { var table = _dataContext.GetTable<Movie>(); var movie = table.SingleOrDefault(m => m.Id == id); movie.Title = title; movie.Director = director; _dataContext.SubmitChanges(); return RedirectToAction("Index"); } } }
The Home controller in Listing 4 uses Dependency Injection. It has two constructors. One constructor is used when the application runs. One constructor is used when creating the controller in a unit test.
When the HomeController is used in production, the parameterless constructor is called. This contructor creates an instance of the DataContextWrapper class. It creates a DataContext by passing two values to the DataContextWrapper constructor. The first value represents a database connection string name (the name of a connection string in the web configuration file). The second value is the path to an XML mapping file that maps properties of the Movie class to columns in the database.
Notice that the second constructor does not accept a DataContextWrapper. Instead, it accepts any class that implements the IDataContext interface. Notice, furthermore, that only the IDataContext interface is used within the body of the Home controller.
Because both the real DataContext (the DataContextWrapper) and the FakeDataContext implements the IDataContext interface, the very same Home controller can be instantiated and executed with either the real DataContext or the fake DataContext. This makes the Home controller very testable.
Let’s examine how each of the methods of the Home controller can be tested. The Index() action returns all of the movies from the database and passes the movies to the Index view through view data (see Figure1). The unit test in Listing 5 verifies that the movie data is actually returned in view data.
Figure 1 – Viewing database records
Listing 5 – TestIndex()
private FakeDataContext _fakeDataContext;
[TestInitialize]
public void Initialize()
{
// Create Fake DataContext
_fakeDataContext = new FakeDataContext();
// Add some fake data
var table = _fakeDataContext.GetTable<Movie>();
table.InsertOnSubmit(new Movie(1, "Star Wars", "Lucas"));
table.InsertOnSubmit(new Movie(2, "Raiders of the Lost Ark", "Speilburg"));
_fakeDataContext.SubmitChanges();
}
[TestMethod]
public void TestIndex()
{
// Arrange
var controller = new HomeController(_fakeDataContext);
// Act
ViewResult result = controller.Index() as ViewResult;
// Assert
ViewDataDictionary viewData = result.ViewData;
var movies = (List<Movie>)viewData.Model;
Assert.AreEqual("Star Wars", movies[0].Title);
}
The code in Listing 5 contains two methods. The first method, named Initialize(), is called once before any of the other unit tests. This method initializes the fake DataContext with two movie records.
The second method, named TestIndex(), tests the Home controller Index() action. This method creates an instance of the HomeController by passing the FakeDataContext to the HomeController class’s constructor. Next, the HomeController.Index() method is called. Finally, the view data returned by calling the Index() method is examined to check for at least the Star Wars movie record. If the record is there, then the Index() method is assumed to have worked.
The unit test in Listing 6 demonstrates how you can unit test code that inserts new data into a database (see Figure 2). This unit test is run against the HomeController.Insert() method.
Figure 2 – Inserting a new record
Listing 6 – TestInsert()
[TestMethod] public void TestInsert() { // Arrange var controller = new HomeController(_fakeDataContext); // Act controller.Insert("Batman", "Burton"); // Assert var table = _fakeDataContext.GetTable<Movie>(); var newMovie = table.SingleOrDefault(m => m.Title == "Batman"); Assert.IsNotNull(newMovie); }
The unit test in Listing 6 creates a new instance of the HomeController class passing the fake DataContext to its constructor. Next, the HomeController.Insert() method is called with the values Batman and Burton. If the HomeControler.Insert() method correctly inserts the new record, then we should be able to retrieve the new record from the fake DataContext. The test attempts to retrieve the movie with the title Batman. If the movie record exists (it is not null) then the test succeeds.
One last demonstration of how you can use the FakeDataContext when building unit tests. The test in Listing 7 tests the Home controller’s Update() method.
Listing 7 – TestUpdate()
[TestMethod] public void TestUpdate() { // Arrange var controller = new HomeController(_fakeDataContext); // Act controller.Update(1, "Batman", "Burton"); // Assert var table = _fakeDataContext.GetTable<Movie>(); var newMovie = table.SingleOrDefault(m => m.Id == 1); Assert.AreEqual("Batman", newMovie.Title); }
The test in Listing 7 calls the HomeController.Update() method passing the new values 1, “Batman”, and “Burton”. Next, the test verifies whether or not the movie record with an Id of 1 has been updated with the new values. If the movie has a title of Batman (instead of its original value Star Wars) then the test succeeds.
Summary
In this tip, I’ve demonstrated a very easy way of testing your LINQ to SQL code. Notice that we did not need to resort to creating a separate Repository class. You can use the LINQ to SQL DataContext directly within your controller actions and you can still test your controllers by taking advantage of the FakeDataContext.