ASP.NET MVC Application Building: Forums #2 – Create the First Unit Test
In this series of blog entries, I build an entire ASP.NET MVC Forums application from scratch. In this blog entry, I create my first unit test for the Forums application and implement the code necessary to pass the test.
Before reading this blog entry, you might want to read the first entry in this series:
ASP.NET MVC Application Building: Forums #1 – Create the Perfect Application – Describes the goals of the ASP.NET MVC Forums application.
Create the ASP.NET MVC Application
Let me start by creating a new ASP.NET MVC application. Launch Visual Studio 2008 and select the menu option File, New Project. Select the ASP.NET MVC Web Application project type, give the project the name MvcForums, and click the OK button.
When the dialog appears that asks whether or not you want to create a unit test project, respond by clicking the OK button (see Figure 1). We need a unit test project because we will be practicing test-driven development. We are going to use Visual Studio Unit Tests to create our unit tests. However, we could use an alternative unit test framework such as NUnit or XUnit.net.
Figure 1 – Create Unit Test Project dialog
After you click OK, a solution is created that contains two projects. The solution contains the MvcForums application project. The solution also contains the unit test project named MvcForumsTests.
The MvcForums project contains a sample controller named HomeController and sample views for the Home controller. I recommend that you delete these files. Delete the following file and folder:
\Controllers\HomeController.cs
\Views\Home
Also, delete the corresponding unit test from the MvcForumsTests project:
\Controllers\HomeControllerTests.cs
Create the Unit Test
When practicing test-driven development, the first step when writing an application is always to write a unit test. We are going to start by creating a test that verifies that the Index() method of the Forum controller returns a list of message threads from the database.
When writing a unit test before writing the application code that satisfies the test, I recommend that you disable Visual Studio automatic statement completion. Otherwise, you will find yourself constantly fighting with Visual Studio as you type your code. You can disable automatic statement completion by selecting the menu option Tools, Options. Select the Text Editor node and uncheck the Auto List Members checkbox (see Figure 2).
Figure 2 – Disabling automatic statement completion
After you disable automatic statement completion, you can still get statement completion when typing in the Visual Studio code editor by hitting the keyboard combination CTRL-SPACE.
This first step is going to be a big step. My first unit test will embody several assumptions about how I will structure my application. The first unit test is contained in Listing 1.
Listing 1 – Controllers\ForumControllerTest.cs
using System.Collections.Generic; using System.Web.Mvc; using LinqToSqlExtensions; using Microsoft.VisualStudio.TestTools.UnitTesting; using MvcFakes; using MvcForums.Controllers; using MvcForums.Models; namespace MvcForums.Tests.Controllers { [TestClass] public class ForumControllerTest { private IDataContext _dataContext; private IForumRepository _repository; [TestInitialize] public void Initialize() { // Setup data context and fake data _dataContext = new FakeDataContext(); _dataContext.Insert(new Message(1, null, "Robert", "Welcome to the MVC forums!", "body1")); _dataContext.Insert(new Message(2, 1, "Stephen", "RE:Welcome to the MVC forums!", "body2")); _dataContext.Insert(new Message(3, 2, "Robert", "RE:Welcome to the MVC forums!", "body3")); _dataContext.Insert(new Message(4, null, "Mark", "Another message", "body4")); _dataContext.Insert(new Message(5, 4, "Stephen", "Yet another message", "body5")); _dataContext.Insert(new Message(6, 5, "Jane", "Yet another message", "body6")); // Create repository _repository = new ForumRepository(_dataContext); } [TestMethod] public void IndexReturnsMessageThreads() { // Arrange var controller = new ForumController(_repository); // Act var result = controller.Index() as ViewResult; // Assert var model = result.ViewData.Model as List<Message>; Assert.AreEqual(2, model.Count); } } }
The unit test class in Listing 1 contains two methods named Initialize() and IndexReturnsMessageThreads(). Notice that the Initialize() method is decorated with the [TestInitialize] attribute. This attribute causes the Initialize() method to be run before each of the unit tests.
The Initialize() method is used to setup a fake DataContext. A number of fake forum messages are added to the fake DataContext. A forum message has the following properties:
· Id
· ParentId
· Author
· Subject
· Body
We are creating a threaded discussion forum. The forum messages are organized into threads. There can be several messages in a thread.
The ParentId property represents the parent message of the current message. When a message has a NULL parent, then that message is assumed to be the first message in a thread.
Because the fake DataContext is set up in the Intialize() method, all of the unit tests in the test class can take advantage of the fake DataContext.
The FakeDataContext class is part of the MvcFakes project. If you have read my previous tips, then you will be familiar with this project. You can learn more about the FakeDataContext class by reading the following blog entry:
The fake DataContext is passed to an instance of the Repository class. The MVC Forums application will take advantage of the Repository pattern in order to break any dependencies on a particular data access technology.
The IndexReturnsMessageThreads() method verifies that the Forums controller Index() method returns a list of message threads. This unit test is divided into three sections.
In the Arrange section, an instance of the Forum controller is created. Notice that the repository is passed to the constructor of the Forum controller when the controller is created. Within the unit test, the Forum controller uses the repository instantiated with the fake DataContext.
Next, in the Act section, the Index() action is invoked. The Index() action returns a ViewResult.
Finally, in the Assert section, the ViewData.Model property is cast to a collection of Message objects. If the Index() method returns all of the threads then the Index() method should return exactly three messages. Remember that a thread is a message with a NULL ParentId.
When you first attempt to run the unit test in Listing 1, the test will fail. In fact, your application won’t even compile. You’ll get the Error List window in Figure 2.
Figure 2 – First run failure
When you first attempt to run the unit test, the unit test will fail because you haven’t written any application code yet. You want your unit tests to fail. That way, when the unit tests pass, you know they passed for a good reason.
In order to get our unit test to pass, we need to create the following objects:
· Message class -- This class represents an individual forum message.
· ForumRepository class – This class is used to retrieve and store forum messages.
· IForumRepository interface – This interface describes the methods of the ForumRepository class.
· ForumController class – This controller class exposes actions for interacting with the forums.
· ForumsDB database – The database for the Forums application.
· Messages table – This database table contains all of the messages.
· ForumsDB.xml – This XML file maps classes in the Forums application to tables in the Forums database.
We’ll create these objects in the following sections.
Creating the Message Class
First, we need to create a Message class that represents a message posted to the forums. This class is contained in Listing 2.
Listing 2 – Models\Message.cs
using System; namespace MvcForums.Models { public class Message { public Message() { } public Message(int id, int? parentId, string author, string subject, string body) { this.Id = id; this.ParentId = parentId; this.Author = author; this.Subject = subject; this.Body = body; } public int Id { get; set; } public int? ParentId { get; set; } public string Author { get; set; } public string Subject { get; set; } public string Body { get; set; } public DateTime EntryDate { get; set; } } }
Creating the Forum Repository
Second, we need to create the ForumRepository class. The Forums application uses the ForumRepository class to interact with the database. This class is contained in Listing 3.
Listing 3 – Models\ForumRepository.cs
using MvcFakes; using System.Linq; using System.Data.Linq; using LinqToSqlExtensions; using Microsoft.Web.Mvc; using System.Collections.Generic; namespace MvcForums.Models { public class ForumRepository : IForumRepository { private IDataContext _dataContext; public ForumRepository() : this(new DataContextWrapper("conForumsDB", "~/Models/ForumsDB.xml")) { } public ForumRepository(IDataContext dataContext) { _dataContext = dataContext; } public IList<Message> SelectThreads() { var messages = _dataContext.GetTable<Message>(); var threads = from m in messages where m.ParentId == null select m; return threads.ToList(); } } }
The ForumRepository class has one public method named SelectTheads() which returns all of the messages with a NULL ParentId. This method returns an IList. The List collection implements the IList interface.
The ForumRepository class supports Constructor Dependency Injection. Notice that the class has two constructors. If you instantiate the class and you do not supply a class that implements the IDataContext interface, then the ForumRepository class defaults to using an instance of the DataContextWrapper class (The DataContextWrapper class is a thin wrapper around the DataContext class that adds the IDataContext interface).
Within a unit test, a fake DataContext is passed to the constructor for the ForumRepository. That way, the ForumRepository can be tested without touching the actual database.
The ForumRepository implements the IForumRepository interface in Listing 4.
Listing 4 – Models\IForumRepository.cs
using Microsoft.Web.Mvc; using MvcFakes; using System.Collections.Generic; namespace MvcForums.Models { public interface IForumRepository { IList<Message> SelectThreads(); } }
Because the ForumRepository takes advantage of LINQ to SQL, you must add a reference to your application to the System.Data.Linq assembly. Select the menu option Project, Add Reference and select the System.Data.Linq assembly beneath the .NET tab.
Creating the Forum Controller
Next, we need to create the ForumController class. A controller is responsible for generating response to user requests. The Forums controller class is contained in Listing 5.
Listing 5 – Controllers\ForumController.cs
using System; using System.Web.Mvc; using MvcForums.Models; namespace MvcForums.Controllers { public class ForumController : Controller { private IForumRepository _repository; public ForumController() : this(new ForumRepository()) { } public ForumController(IForumRepository repository) { _repository = repository; } public ActionResult Index() { ViewData.Model = _repository.SelectThreads(); return View("Index"); } } }
The Forum controller also takes advantage of Constructor Dependency Injection. When the ASP.NET MVC framework instantiates the ForumController class within a running application, the parameterless constructor is used. This constructor creates an instance of the ForumRepository class that accesses the actual database. Within a unit test, on the other hand, the constructor that accepts a ForumRepository is used. In a unit test, a repository constructed with a fake DataContext is passed to the Forum controller.
Creating the Database Objects
Next, we need to create our database objects. We need to create the database itself and a database table named Messages.
I’m using Microsoft SQL Server Express when building the MVC Forums application. When I am ready to place the application into production, I can easily switch to the full version of Microsoft SQL Server.
You create a local user instance of a SQL Server Express database by right-clicking the App_Data folder, selecting the menu option Add, New Item, and selecting the SQL Server Database template (see Figure 3).
Figure 3 – Creating a new SQL Express database
After you add the new database, you can double-click the database to open the Server Explorer window. Within the Server Explorer window, you can manage your database objects.
Right-click the Tables folder and select the menu option Add New Table to add a new table to the database. I created a Messages table with the columns displayed in Figure 4.
Figure 4 – Creating the Messages database table
The only special column in the Messages table is the Id column. This column is both a primary key column and an Identity column.
After you create the database and database table, you need to add the following entry to the connectionStrings section of the web configuration (web.config) file:
<add name="conForumsDB" connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|forumsdb.mdf;User Instance=true" providerName="System.Data.SqlClient"/>
This connection string is used in the ForumRepository class to connect to the database. The easiest way to add this connection string is to copy the existing ApplicationServices connection string and modify the name to be conForumsDB and the name of the database to be forumsdb.mdf.
Mapping Application Classes to Database Objects
The final file that we need to create is the XML file that maps the Message class to the Messages database table. This file is contained in Listing 6.
Listing 6 – Models\ForumsDB.xml
<?xml version="1.0" encoding="utf-8" ?> <Database Name="ForumsDB" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007"> <Table Name="Messages" Member="MvcForums.Models.Message"> <Type Name="MvcForums.Models.Message"> <Column Name="Id" Member="Id" IsDbGenerated="true" IsPrimaryKey="true" /> <Column Name="ParentId" Member="ParentId" /> <Column Name="Author" Member="Author" /> <Column Name="Subject" Member="Subject" /> <Column Name="Body" Member="Body" /> <Column Name="EntryDate" Member="EntryDate" /> </Type> </Table> </Database>
If you have Visual Studio 2008 Service Pack 1 installed then you get Intellisense while building the XML file just as soon as you add the xmlns=” http://schemas.microsoft.com/linqtosql/mapping/2007” attribute (you don’t need t enter the value of this attribute, just hit CTRL-Space).
Success!
After you add all of the files in the sections above, the ForumController unit test will pass (see Figure 5). The test verifies that the Forum controller Index() action returns all of the threads from the database. Notice that we don’t actually need to run the application to check whether the application is working. The unit test provides us with instant reassurance.
Figure 5 – Success!
After you start writing unit tests, you quickly become addicted to them. They provide you with a safety net for your code. Unit tests enable you to redesign your code at any time in the future without you having to worry about breaking existing code.
A Moment of Reflection
Selecting what code to test first is always controversial. I decided to test the Forum controller Index() method in my first unit test. I wanted to verify that I can return a list of message threads from the database.
Other developers who practice test-driven development might have focused on testing some other aspect of the software first. For example, someone might argue that it would have made more sense to create a set of tests around the ForumRepository before creating a test for the ForumController.
Here’s how I decided what to test first. I focused on what I want my application to do. In this case, I focused on the fact that I want my Forums application to be able to return messages. Therefore, I started by creating a unit test that verifies that my application satisfies this requirement. The ForumRespository class and the Message class just seems like necessary plumbing required to get the Index() action to work.
Later down the road, I might discover that I need to create unit tests for the ForumRepository itself. If I add functionality to the ForumRepository class that is not directly exposed through a controller action then I would start to write unit tests specifically around the ForumRepository.
Right now, however, I am confident that the Forums application works in the way that I intend it to work. My goal was to get the Index() action to return message threads and I have satisfied this goal.
Summary
In this blog entry, we’ve taken our first steps in the journey to build the perfect MVC Forums application. In the next entry, we tackle the issue of inserting new messages into the database.