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

clip_image002

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

clip_image004

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:

http://weblogs.asp.net/stephenwalther/archive/2008/08/16/asp-net-mvc-tip-33-unit-test-linq-to-sql.aspx

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

clip_image006

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

clip_image008

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

clip_image010

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!

clip_image012

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.

Download the Code

13 Comments

  • Update so fast,You are really a great man,Mr Walther!

  • Nice stuff! But you mention ForumsDB.xml for mapping the db object to your model. While it is clear what the file is for, I don't see any description of how this works and I've not seen this method mentioned before in an MVC discussion or in Linq to SQL discussion. Is this your own home-grown mapping or is this built in to the framework somewhere?

  • @Trevor -- Great question! The external XML file is a LINQ to SQL option. I wrote a blog entry on this approach here:

    http://weblogs.asp.net/stephenwalther/archive/2008/07/22/asp-net-tip-23-use-poco-linq-to-sql-entities.aspx

  • @geek50 - After you download the code, make sure that you right-click the zip file, select Properties, and click the Unblock button. Otherwise, you run into security issues when attempting to run the code in Visual Studio. I hope this fixes the problem. Let me know if it doesn't.

  • if you are going to write code this badly, at least do it off the internets.

  • Fail fast is also a best practice. However, I notice a lot of "as" casts creeping in to MVC sample code which would cause failures in subsequent lines. For some reason, these seem particularly common in ASP.NET MVC sample unit tests.

    For example, you have in your tests:

    var model = result.ViewData.Model as List;

    Why not do a simple cast so that it fails immediately and meaningfully? Doing an "as" cast fails silently with the result that you'll get a confusing null reference exception on the next statement, instead of a clear "type x cannot be cast to type y..." error.

  • @ceilidhboy -- good point! I'll update the test code in the next iteration to do an immediate cast.

  • After reading your article, I'm confused by the dll MvcFakes, what is it? The dll belong to .NET Framework 3.5 or built by yourself?

  • This is looking nice. I appreciate the fact that you are taking the time to write this down.

    I have never worked like, but I plan to. One thing I worry about is 'faking it'. Isn't mocking a better way? (I'm just repeating what I heard)

    -m

  • Really good work! Thanx for contributing!

    I have a question regarding the MvcFakes project, where can I find the code for it? If I download the code from your tip #33 then when trying to use it in the Forums app the first test keeps failing with the following message: Error 1 'MvcFakes.IDataContext' does not contain a definition for 'Insert' and no extension method 'Insert' accepting a first argument of type 'MvcFakes.IDataContext' could be found (are you missing a using directive or an assembly reference?) C:\Documents and Settings\frederikw\Mina dokument\Visual Studio 2008\Projects\MvcForums\MvcForumsTests\Controllers\ForumControllerTest.cs 24 13 MvcForumsTests

  • I am getting the same build error: 'MvcFakes.IDataContext' does not contain a definition for 'Insert'...

  • The trick is in the "using LinqToSqlExtensions". Add this to your file and the Insert extension method will become available.

  • mire se erdhet te forumi ime...dj titi

Comments have been disabled for this content.