【IT168 技术文档】【提要】从本篇开始,我将着手创建我的论坛应用程序中的第一个单元测试,并且将实现必要的代码以便顺利通过这个测试。
一、 创建ASP.NET MVC应用程序
让我们首先创建一个新的ASP.NET MVC应用程序。为此,启动VisualStudio2008并且选择菜单选项“File—New Project”,然后从工程模板中选择ASP.NET MVC Web应用程序工程类型,并给该工程命名为MvcForums,最后点击OK按钮。
当随后出现一个对话框询问你是否想创建一个单元测试工程时,请点击其中的OK按钮(见图1)。在本文示例程序中,我们需要一个单元测试工程,因为我们将在本应用程序中遵循测试驱动开发的方式进行编程。而且,我们还将使用Visual Studio内置的单元测试框架(即Visual Studio Unit Tests)来创建我们的单元测试.然而,我们也可以使用其他可选的单元测试框架,例如NUnit或XUnit.net。
图1—创建单元测试工程对话框
在你点击OK后,一个包含两个工程的方案即被创建。该方案包含一个Mvc论坛应用程序工程,还包含一个名字为MvcForumsTests的单元测试工程。
其中,MvcForums工程包含一个名字为HomeController的示例控制器和相应于Home控制器的视图。作为一般的惯例,我推荐你删除这些文件。也就是说,请删除下列文件和文件夹:
\Controllers\HomeController.cs
\Views\Home
此外,还需要从MvcForumsTests工程中删除如下相应的单元测试:
\Controllers\HomeControllerTests.cs
【注意】删除以上内容后,你可以方便地创建你自己的控制器和视图以及单元测试文件。
二、 编写单元测试
当遵循测试驱动开发原则开发软件时,编写一个应用程序的第一个步骤总是从单元测试的编写开始。下面,我们将着手编写一个单元测试,它将校验论坛控制器的Index()方法能够成功地返回一个来自于服务器端数据库的消息线程的列表。
在编写满足测试的应用程序代码之前当编写一个单元测试,我推荐你禁用Visual Studio的自动语句完成支持;否则,你会发现Visual Studio总是不停地干扰你输入你的代码。你可以禁止使用这个自动语句完成支持。方法是选择菜单“Tools—Options”,然后选择“Text Editor”结点,最后在右侧取消选择“Auto list members”复选框即可(见图2)。
图2–禁用Visual Studio的自动语句完成功能
【提示】在你禁止使用自动语句完成功能后,你仍然可以取得自动语句完成功能的支持,当你在Visual Studio代码编辑器中按下组合键CTRL+SPACE时。
上面这个第一个步骤涉及到许多的内容。我的第一个单元测试将体现出有关本应用程序构架的若干假定。列表1中提供了第一个单元测试的相应代码。
列表1–Controllers\ForumControllerTest.cs
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()
{
//创建数据上下文和虚构的数据
_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"));
//创建仓库
_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);
}
}
}
列表1中的单元测试类包含了两个方法,名字分别为Initialize()和IndexReturnsMessageThreads()。注意,Initialize()方法前面修饰有[TestInitialize]属性。这个属性能够确保在每一个单元测试之前运行Initialize()方法。
Initialize()方法用于创建一个模拟的DataContext。一些模拟的论坛消息将被添加到该模拟的DataContext中。注意,一个论坛消息拥有下列属性:
? Id
? ParentId
? Author
? Subject
? Body
注意,我们现在创建的是一个基于线程的讨论某些主题的论坛程序。其中的论坛消息都是以线程的方式加以组织的。因此,在一个线程中可能存在若干条消息。
上面的ParentId属性描述了当前消息的父级消息。如果一条消息拥有一个NULL类型的父级消息时,那么,此消息将被假定为是这个线程中的第一条消息。
因为模拟的DataContext是在Intialize()方法中建立的,所以,测试类中所有的单元测试都可以利用这个模拟的DataContext。
注意,FakeDataContext类是作为MvcFakes工程的一部分存在的,详情请参考本文所附源码。
上述虚构的DataContext将被传递到Repository类的一个实例中。MVC论坛应用程序将利用Repository模式,其目的是为了解除系统对于任何特定的数据访问技术的依赖性。
接下来请注意,IndexReturnsMessageThreads()方法负责校验Forums控制器中的Index()方法确保其返回一个消息线程的列表。这个单元测试被分成三个部分。
在Arrange部分中,将创建一个Forum控制器的实例。注意,当创建控制器时,该repository将被传递到Forum控制器的构造器。在这个单元测试中,Forum控制器将使用借助于虚拟的DataContext实例化的repository。
接下来,在Act部分中,Index()动作被调用。该Index()动作返回一个ViewResult。
最后,在Assert部分中,ViewData.Model属性被强制转换成一个消息对象的集合。如果Index()方法返回所有的线程,那么该Index()方法应该正好返回三个消息。记住,一个线程就是一个ParentId为NULL的消息。
当你第一次试图运行列表1中的单元测试时,该测试将会失败。事实上,你的应用程序甚至不会通过编译—你将得到如图3所示的错误列表窗口。
图3—第一次运行单元测试失败
当你第一次试图运行上面的单元测试时,该单元测试将会失败。原因很简单,因为你还没有编写任何应用程序代码。你要使你的单元测试失败。通过这种方式,当时该单元测试最终通过时,你便知道它们通过的良好理由。
为了使我们的单元测试顺利通过,我们需要创建下列对象:
Message类—这个类描述一条论坛消息。
ForumRepository类—这个类用于检索和存储论坛消息。
IForumRepository接口—这个接口用于描述ForumRepository类中的方法。
ForumController类—这个控制器类将负责暴露一系列与论坛发生交互的动作。
ForumsDB数据库—这是相应于论坛应用程序的数据库。
Messages表格—这个数据库表格包含了所有的论坛消息。
ForumsDB.xml—这个XML文件将负责把论坛应用程序中定义的类映射为对应的论坛数据库表格。
在接下来的几节中,我们将创建这些对象。
三、 创建Message类
首先,我们需要创建一个消息类,它用于描述一条提交到论坛的消息。列表2提供了有关这个类的完整代码。
列表2–Models\Message.cs
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; }
}
}
四、 创建论坛仓库类
接下来,我们需要创建一个ForumRepository类。论坛应用程序将使用该ForumRepository类实现与数据库的交互。列表3提供了这个类的实例代码。
列表3—Models\ForumRepository.cs
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();
}
}
}
首先注意到,上面这个ForumRepository类提供了一个名字为SelectTheads()的public方法。此方法负责返回所有的ParentId为NULL的消息。这个方法返回参数形式为一个IList—该列表集合实现IList接口。
另外还应注意到,这个ForumRepository类支持构造器依赖性注入。这个类有两个构造器函数。如果你实例化该类,但是没有提供一个实现了IDataContext接口的类,那么ForumRepository类默认会使用一个DataContextWrapper类的实例(其中,DataContextWrapper类是一个添加了IDataContext接口的DataContext类的瘦包装器)。
在单元测试,我们可以把一个虚构的DataContext传递到上面ForumRepository类的构造器中。通过这种方式,我们就可以对ForumRepository类进行测试而不必接触到实际的数据库。
列表4给出了实现了IForumRepository接口的ForumRepository类的完整定义。
列表4–Models\IForumRepository.cs
using MvcFakes;
using System.Collections.Generic;
namespace MvcForums.Models
{
public interface IForumRepository
{
IList<Message> SelectThreads();
}
}
【注意】因为ForumRepository类中利用了LINQ to SQL技术,所以你必须在你的应用程序中添加一个对程序集System.Data.Linq的引用。
五、 创建论坛控制器
接下来,我们需要创建论坛控制器—ForumController。此控制器将会负责针对用户请求生成响应。其实现代码列举于列表5中。
列表5–Controllers\ForumController.cs
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");
}
}
}
另外还应注意到,该论坛控制器还利用了构造器依赖性注入技术。当ASP.NET MVC框架在一个运行时刻应用程序中实例化ForumController类时将使用无参的构造器。这个构造器能够创建访问实际数据库的ForumRepository类的一个实例。另一方面,在单元测试内部,则使用构造器接收一个ForumRepository类型参考的构造器。在单元测试中,我们将把一个带有一个虚构的DataContext的repository传递到论坛控制器。
六、 创建数据库对象
接下来,我们需要创建我们的数据库对象。我们需要创建数据库本身,还有一个名字为Messages的数据库表格。
注意,在构建该MVC论坛应用程序时我使用的是Microsoft SQL Server Express数据库。使用这种方式,当我为把该应用程序投入实际运行环境时,我可以很容易地切换到完整版本的Microsoft SQL Server。
为了创建SQL Server Express数据库的一个本地的用户实例,你仅需右击示例网站的App_Data文件夹,然后从弹出菜单中选择“添加新项”并且在随后的弹出对话框中选择SQL Server Database模板(见图4)。
图4—创建一个新的SQL Express数据库
当你添加完新的数据库后,你可以双击该数据库来打开服务器资源管理器窗口。在服务器资源管理器窗口中,你可以很方便地管理你的数据库对象。
右击Tables文件夹,然后选择菜单项“Add New Table”把一个新的表格添加到数据库中。在本例中我创建的Messages表格有关字段显示于图5中。
图5—创建Messages数据库表格
这个Messages表格仅有的一个特殊字段为Id列对应的字段。这一栏既是一个主键字段也是一个标识字段。
在成功创建完数据库和数据库表格后,你需要把下列入口添加到web配置文件(web.config)的connectionStrings节中:
name="conForumsDB"
connectionString="data source=.\SQLEXPRESS;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|forumsdb.mdf;User Instance=true"
providerName="System.Data.SqlClient"/>
上面这个连接字符串将被应用于ForumRepository类中以连接到数据库。添加这个连接字符串的最简单的方法是复制现有ApplicationServices连接字符串,然后仅需简单地把连接字符串的name修改为conForumsDB并且把数据库名改为forumsdb.mdf即可。
七、 把应用程序类映射到数据库对象
我们最后需要创建的文件是负责把把消息类映射到消息数据库表格的XML文件。列表6提供了这个文件的相关源码。
列表6—Models\ForumsDB.xml
<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>
【注意】如果你安装了Visual Studio 2008 Service Pack 1,那么在创建上面的这个XML文件时你还可以得到智能感知的支持(只要你添加了xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007"属性,当然为了输入这个属性值你不需要输入完整的内容而仅需按下组合键CTRL+Space即可)。
八、 测试成功
在你添加完前面几节中所有的相关的文件后,便会顺利通过ForumController的单元测试,测试成功的快照请参考图6。该测试验证Forum控制器的Index()动作的确是返回了数据库中所有的线程。注意,我们不需要实际地运行该应用程序去检查是否应用程序能够正确工作。单元测试能够提供给我们足够的安全保证。
图6—顺利通过测试的快照
当你开始编写单元测试时,你很快就会迷恋于它们,因为它们能够为你的代码提供一个安全的保证。单元测试能够使你在以后的任何时候更新你的代码而不必担心破坏现有代码。
九、 最后的一点思考
选择首先测试什么样的代码总是一个有争议的话题。我决定在第一个单元测试中首先测试论坛控制器中的Index()方法。我的目的是想验证一下我的确可以从数据库返回一个消息线程的列表。
当然,其它也遵循测试驱动开发的开发者有可能首先想到测试该软件的其它一些方面。例如,有人可能争论:在为ForumController创建一个测试之前,首先创建一组针对ForumRepository的测试似乎更有意义。
下面我说一下我是如何决定首先测试的内容的。我重点关注的是我希望我的应用程序完成什么任务。在本文论坛示例中,我关心的事实是:我想使这个论坛应用程序能够正确地返回消息。因此,我开始创建一个能够使我的应用程序满足这个要求的单元测试。本文中所创建的ForumRespository类和Message类正是必要的工具类,借助于它们便可以使Index()动作按既定方式正确工作。
沿着这种思路,以后我进一步发现我可能需要为ForumRepository本身创建一个单元测试。如果我在ForumRepository类添加一些无法直接通过一个控制器动作暴露的功能,那么我必须先开始编写专门针对ForumRepository的单元测试。
然而,现在我坚信我的论坛应用程序目前的确能够按照我的既定设计正确地工作。我的目标是使Index()动作返回消息线程,而我最终也实现了这个目标。
十、 总结
在本篇中,我们已经正式踏上了构建一个完美的MVC论坛应用程序的旅程。在接下来的下一篇中,我们将着重解决如何把新消息插入到数据库的问题。