Unit testing: decoupling the database

In the last post, we saw how to add a simple unit test to an MVC4 project. At the time, we noted that our ComicShop project wasn’t in the best form for unit testing, since the HomeController accessed the database (in which the comic books are stored) directly. This means that any test of HomeController required interacting with the database, so we’re not testing just the code in the controller in isolation.

Here, we’ll see how to decouple the database from the controller, and in the process, how to add in a localized test for the controller that is independent of the database.

The idea is to define a C# interface whose job it is to provide the connection to the data source. In the functioning application, this interface is implemented by a class that does connect to the database, but for testing purposes, we use another class that also implements this interface, but which provides a test set of data without referring to a database.

To design this interface, we look at the HomeController code, and note that there are, at the moment, only two methods that interact with the database: Index(), which retrieves the list of Books in the database, and the HttpPost version of Add(), which takes data off a form submitted by the user and adds this to the database. The other Add() method just displays a form in which the user enters data; no connection to the database is used here.

Our interface, then, should contain two methods which, in classes implementing the interface, are used to connect to the database in one case and to some local data source in the testing case. Here’s the definition of the interface:

using System.Collections.Generic;

namespace ComicShop.Models
{
  public interface IComicRepository
  {
    List<Book> GetComicList();
    void AddComic(Book book);
  }
}

We’ve added this interface to the Models namespace, since it deals with data storage, and classes that implement it are model classes.

We can now define a class called ComicRepository which implements IComicRepository and move the code relying on the database from HomeController to ComicRepository. This class is as follows:

using System.Collections.Generic;
using System.Linq;

namespace ComicShop.Models
{
  public class ComicRepository : IComicRepository
  {
    public ComicContext database = new ComicContext("ComicContextDb");
    public List<Book> GetComicList()
    {
      var comics = database.ComicBooks.Select(book => book).OrderBy(book => book.Title);
      return comics.ToList();
    }

    public void AddComic(Book book)
    {
      database.ComicBooks.Add(book);
      database.SaveChanges();
    }
  }
}

GetComicList() contacts the database and retrieves the list of books using the same LINQ code as we had in HomeController before. However, it converts the result to a List<Book> and returns it. There is no mention of a View, since that’s the job of the controller; this class should concern itself purely with manipulating the data.

In a similar vein, AddComic() accepts a Book and adds it to the database.

The new version of HomeController now looks like this:

using System.Web.Mvc;
using ComicShop.Models;

namespace ComicShop.Controllers
{
  public class HomeController : Controller
  {
    private IComicRepository comicRepository;

    public HomeController()
    {
      comicRepository = new ComicRepository();
    }

    public HomeController(IComicRepository comicRepository)
    {
      this.comicRepository = comicRepository;
    }

    public ViewResult Index()
    {
      var model = comicRepository.GetComicList();
      return View("IndexModel", model);
    }

    public ActionResult Add()
    {
      return View();
    }

    [HttpPost]
    public ActionResult Add(Book book)
    {
      comicRepository.AddComic(book);
      return Content("New comic added.");
    }
  }
}

All explicit references to the database have been eliminated. The interaction with the data source is done entirely through the comicRepository object.

A couple of important additions have been made: we now have two constructors in HomeController.

The argumentless constructor initializes the comicRepository object to ComicRepository, the class that accesses the database. We want this to be the default behaviour of the controller, since this is what is done when we are running the web site.

The second constructor allows us to specify the repository used by HomeController. We’ll use this constructor when we write the test class.

First, we need to define a test class that implements IComicRepository. This class should use a local data source without any reference to the database. Since the web site uses a List<Book> as its working data structure, we can use this as the test data source. We therefore define ComicRepositoryTest like this:

using System.Collections.Generic;
using ComicShop.Models;

namespace ComicShop.UnitTests.Models
{
  public class ComicRepositoryTest : IComicRepository
  {
    private List<Book> comicRepository;

    public ComicRepositoryTest()
    {
      comicRepository = new List<Book> {
        new Book {
          Title = "Incredible Hulk", Volume = 3, Issue = 134
        },
        new Book {
          Title = "Superboy", Volume = 1, Issue = 17
        }
      };
    }

    public List<Book> GetComicList()
    {
      return comicRepository;
    }

    public void AddComic(Book book)
    {
      comicRepository.Add(book);
    }
  }
}

We’ve put this class in the namespace ComicShop.UnitTests.Models (you’ll need to create a new folder in your tests project in Visual Studio), since it’s a data class. Since it implements IComicRepository, we need a ‘using ComicShop.Models’ at the top.

This class has a constuctor that initializes the List<Book> and adds a couple of Books to it. Note that although this class is in the UnitTests namespace, it’s not a TestClass itself; it’s just an auxiliary class that is used in the testing.

We then implement the two methods in IComicRepository. GetComicList() just returns the List<Book>, and AddComic() adds the Book to the List<Book>.

We can now go back to HomeControllerTests (that we started in the last post), and rewrite the tests properly. The class now looks like this:

using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using ComicShop.Controllers;
using ComicShop.Models;
using ComicShop.UnitTests.Models;
using System.Web.Mvc;

namespace ComicShop.UnitTests.Controllers
{
  [TestClass]
  public class HomeControllerTests
  {
    [TestMethod]
    public void Index_ValidView()
    {
      HomeController homeController = new HomeController(
        new ComicRepositoryTest());
      var result = homeController.Index();
      Assert.IsNotNull(result);
    }

    [TestMethod]
    public void Add_AddBook()
    {
      HomeController homeController = new HomeController(
        new ComicRepositoryTest());
      homeController.Add(
        new Book
        {
          Title = "Iron Man",
          Volume = 3,
          Issue = 57,
          Publisher = "Marvel"
        }
      );
      var result = (ViewResult)homeController.Index();
      var modelList = (List<Book>)result.Model;
      Assert.IsTrue(modelList.Count == 3, "Count is {0}", modelList.Count);
    }
  }
}

Each test should be entirely self-contained, so we don’t define any class variables; we create an instance of HomeController within each TestMethod. For the tests, we pass an instance of ComicRepositoryTest to the HomeController constructor, so HomeController uses the local data source rather than the database. The Index_ValidView() test is the same test we wrote in the previous post, except this time there is no interaction with the database.

We’ve added another TestMethod to test adding a Book to the data source. Add_AddBook() calls HomeController’s Add() method with a Book that is created on the spot, rather than using data sent in by the user from a web page. We then retrieve the list of Books by calling Index() and extracting the Model field from the ViewResult. Since the list defined in ComicRepositoryTest had two Books in it and we’ve added another one, the size of the list should now be 3, so that’s what we test in the Assert.IsTrue() statement.

Some of Assert’s methods allow an optional message to be printed out if the test fails. In this case, the last two parameters sent to Assert.IsTrue() are a string and its parameter. If the Count of items in the list is not 3, we’d like to see how many items are in the list, so we print that out. You can test this feature by changing the test so it fails (by testing if Count is 4, say). To see this message, click on the failed test in Visual Studio’s Test Explorer, and the results should show up at the bottom of the panel.

We could go on and add more tests to the ComicShop project, but you should get the idea at this stage.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Trackbacks

  • By MVC: the view model « Programming tutorials on September 19, 2012 at 2:13 PM

    […] the process out to a separate class also fits in with the design we adopted when implementing unit testing, since we’d like to be able to test the summary-generation code in the controller without […]

  • By MVC: user input model « Programming tutorials on September 20, 2012 at 4:23 PM

    […] UpdateReadStatus() to a method called UpdateReadStatus() in the ComicRepository. This allows us to decouple the controller from the access to the data source, as we’ve done so far in order to enable unit testing. The […]

  • By JSON with ASP.NET MVC 4 « Programming tutorials on October 24, 2012 at 3:46 PM

    […] controller uses the IComicRepository interface and ComicRepository class that we introduced earlier. This controller contains two methods of […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: