Action-packed controllers
4.3 Introduction to unit testing
4.3.2 Testing the GuestbookController
One of the issues with the current implementation of the GuestbookController is that it directly instantiates and uses the GuestbookContext object, which in turn accesses the database. This means that it isn’t possible to test the controller without also having a database set up and correctly populated with test data, which is an inte-gration test rather than a unit test.
Instantiate
Although integration testing is very important to ensure that the different compo-nents of an application are interacting with each other correctly, it also means that if we’re only interested in testing the logic within the controller, we have to have the overhead of making database connections for every test. For a small number of tests this might be acceptable, but if you have hundreds or thousands of tests in a project, it will significantly slow down the execution time if each one has to connect to a data-base. The solution to this is to decouple the controller from the GuestbookContext.
Instead of accessing the GuestbookContext directly, we could introduce a repository that provides a gateway for performing data-access operations on our GuestbookEntry objects. We’ll begin by creating an interface for our repository:
public interface IGuestbookRepository {
IList<GuestbookEntry> GetMostRecentEntries();
GuestbookEntry FindById(int id);
IList<CommentSummary> GetCommentSummary();
void AddEntry(GuestbookEntry entry);
}
This interface defines four methods that correspond to the four queries that we cur-rently have in our GuestbookController. We can now create a concrete implementa-tion of this interface that contains the query logic:
Figure 4.5 Running MSTest unit tests inside Visual Studio
public class GuestbookRepository : IGuestbookRepository {
private GuestbookContext _db = new GuestbookContext();
public IList<GuestbookEntry> GetMostRecentEntries() {
return (from entry in _db.Entries orderby entry.DateAdded descending select entry).Take(20).ToList();
}
public void AddEntry(GuestbookEntry entry) {
entry.DateAdded = DateTime.Now;
_db.Entries.Add(entry);
_db.SaveChanges();
}
public GuestbookEntry FindById(int id) {
var entry = _db.Entries.Find(id);
return entry;
}
public IList<CommentSummary> GetCommentSummary() {
The concrete GuestbookRepository class implements our new interface by providing implementations of all of its methods. We’re using the same query logic that we’d previously placed in the controller, but we’ve now encapsulated our queries in one place. The controller itself can now be modified to use the repository rather than the GuestbookContext directly.
public class GuestbookController : Controller {
private IGuestbookRepository _repository;
public GuestbookController() {
_repository = new GuestbookRepository();
Listing 4.9 The GuestbookRepository
Listing 4.10 Using the repository in the GuestbookController
Implements
}
public ActionResult Index() {
var mostRecentEntries = _repository.GetMostRecentEntries();
return View(mostRecentEntries);
}
public ActionResult Create() {
return View();
}
[HttpPost]
public ActionResult Create(GuestbookEntry entry) {
public ViewResult Show(int id) {
var entry = _repository.FindById(id);
bool hasPermission = User.Identity.Name == entry.Name;
ViewBag.HasPermission = hasPermission;
return View(entry);
}
public ActionResult CommentSummary() {
var entries = _repository.GetCommentSummary();
return View(entries);
} }
Rather than instantiating the GuestbookContext, we now store an instance of our repository within a field
B
. The controller’s default constructor (which will be invoked by the MVC framework when we run the application) populates the field with the default implementation of the repositoryC
. We also have a second constructorD
, which allows us to provide our own instance of the repository rather than the default.This is what we’ll use in our unit tests to pass in a fake implementation of the repository.
Finally, the actions in our controller now use the repository to perform data access rather than executing LINQ queries directly.
Allows repository to be injected
D
NOTE Although we’ve moved the querying logic out of the controller, it’s still important that the query itself should be tested. However, this would not be part of a unit test but rather an integration test that exercises the concrete repository instance against a real database.
At this point, we’re able to test our controller actions in isolation from the database, but to achieve this we’ll need a fake implementation of our IGuestbookRepository interface that doesn’t interact with the database. There are several ways to achieve this— we could create a new class that implements this interface but performs all operations against an in-memory collection (shown in listing 4.11), or we could use a mocking framework such as moq or Rhino Mocks (both of which can be installed via NuGet) to automatically create the fake implementations of our interface for us.
public class FakeGuestbookRepository : IGuestbookRepository {
private List<GuestbookEntry> _entries = new List<GuestbookEntry>();
public IList<GuestbookEntry> GetMostRecentEntries() {
public void AddEntry(GuestbookEntry entry) {
_entries.Add(entry);
}
Listing 4.11 A fake implementation of IGuestbookRepository Dependency injection
The technique of passing dependencies into the constructor of an object is known as dependency injection. However, we’ve been performing the dependency injection manually by including multiple constructors in our class. In chapter 18, we’ll look at how we can use a dependency injection container to avoid the need for multiple constructors. More information about dependency injection can also be found in the book Dependency Injection in .NET by Mark Seemann (http://
manning.com/seemann/) as well as in numerous online articles, such as
“Inversion of Control Containers and the Dependency Injection Pattern” by Martin Fowler (http://martinfowler.com/articles/injection.html).
List used for storage
B
public GuestbookEntry FindById(int id) {
return _entries.SingleOrDefault(x => x.Id == id);
}
public IList<CommentSummary> GetCommentSummary() {
The fake implementation of our repository exposes the same methods as the real ver-sion, except internally it simply makes use of an in-memory collection
B
and both the GetCommentSummary and GetMostRecentEntries methods return canned responses (they always return the same fake data).As our controller contains several actions, there are potentially quite a few tests that we could write. The following listing shows a couple of tests for the Index action:
[TestMethod]
public void Index_RendersView() {
var controller = new GuestbookController(
new FakeGuestbookRepository());
var result = controller.Index() as ViewResult;
Assert.IsNotNull(result);
}
[TestMethod]
public void Index_gets_most_recent_entries() {
var controller = new GuestbookController(
new FakeGuestbookRepository());
var result = (ViewResult)controller.Index();
var guestbookEntries = (IList<GuestbookEntry>) result.Model;
Assert.AreEqual(1, guestbookEntries.Count);
}
The first of our tests invokes the Index action and simply asserts that it renders a view (much like the tests for the HomeController). The second test is slightly more complex—
it asserts that a list of GuestbookEntry objects was passed to the view (if you remember, the Index action invokes the GetMostRecentEntries method of our repository).
Both tests make use of the fake repository
B
. By passing it to the controller’s con-structor, we ensure that the controller uses our fake set of in-memory data rather than connecting to the real database.Listing 4.12 Testing the Index action
Pass fake repository to controller
B
In this section, you saw that you can use unit testing to verify that your controller actions are doing what you expect them to. We wrote some tests to verify that a couple of the actions in the GuestbookController did what we expected, but we also saw that we had to make some changes to the controller in order for it to be easily unit-testable.
If you design your applications with testability in mind, this will avoid the need to per-form subsequent refactorings for testability.
4.4 Summary
In this chapter, we looked in more detail at controllers in the context of our example Guestbook application. You saw that there are several ways to indicate that a class is a controller, although most of the time you’ll inherit from the Controller base class.
You also saw that controller actions don’t have to return views—there are many other types of ActionResults available, and you can even render content directly from an action. From this you can see that controller actions aren’t limited to just rendering views and that you can customize your controller actions to return the type of content that you need for a particular scenario. You can even create your own custom action results if you need to send a response from a controller action that the framework doesn’t support by default (we’ll look at this in chapter 16).
Following this, we looked at some operations that would typically be part of a con-troller action, such as mapping view models and validation. Both of these are com-mon scenarios that you’ll typically end up doing very often in your applications, so it’s important to understand how to do them. We’ll dig into both of these topics in more detail later—we’ll cover many options available for validation in chapter 6, and map-ping view models is the subject of the next chapter.
Finally, we looked at the default unit testing project and at how you can perform assertions on the results of controller actions to make sure a controller action is work-ing correctly.
We’ve now finished the introductory part of the book—in the next part, we’ll move away from the Guestbook application that we’ve used so far and begin to focus on more advanced topics related to ASP.NETMVC development. We’ll begin by exploring the topic of view models, which we mentioned briefly in this chapter, in more detail.
Unit testing vs. TDD
The examples in this section have followed a fairly traditional unit testing approach, where the tests have been written after the code in order to validate its behavior. If we were using TDD (test-driven development), both the tests and the code would be written in small iterations: first write a failing test, then the code to make it pass. This usually means that much less time is spent debugging code, because it leads to a workflow in which you are constantly creating small working chunks.