【IT168 技术文档】本篇中,我们将继续在论坛应用程序中添加一些新的功能以支持用户提交新的消息并进行相应的应答支持。
一、 关于Message类的重新思考
随着程序的不断进展,我很快发现,我最早设计的那个Message类设计根本不会工作。开始时,此Message类被假定为描述一条寄送到论坛的消息。然后,与此相同的Message类用于描述一个线程中的原始消息以及对于此线程的所有的应答消息。
最初的Message类包括下列属性:
Id
ParentId
Author
Subject
Body
EntryDate
其中,ParentId属性描述的消息是当前消息的原始消息(即当前消息是一条应答消息)。例如,如果你正在对消息1作出回答,那么ParentId就是1。如果该消息开始一个新的线程,那么ParentId就是NULL。
遗憾的是,我发现我还需要添加一个ParentThreadId属性;否则,检索一个特定的线程中的所有消息的数据库逻辑简直是太丑陋了。因此,我修改了一下该Message类并使支持下列属性:
Id
ParentThreadId
ParentMessageId
Author
Subject
Body
EntryDate
注意,上面这个Message类现在拥有两个新的属性:一个是ParentThreadId属性,另一个是ParentMessageId属性。我使用ParentThreadId属性检索相同线程中的所有消息。此外,该ParentMessageId属性还可以应用于一个视图中以显示相同的线程中不同的消息之间的关系。
二、 创建新的单元测试
因为我正在遵循测试驱动开发思想构建本文中的论坛应用程序,所以我总是先从创建一组新的单元测试开始。列表1显示了这一组新的单元测试。
列表1—Controllers\ForumControllerTest.cs
using System.Web.Mvc;
using LinqToSqlExtensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Web.Mvc;
using MvcFakes;
using MvcForums.Controllers;
using MvcForums.Models;
namespace MvcForums.Tests.Controllers
{
[TestClass]
public class ForumControllerTest
{
private IForumRepository _repository;
[TestInitialize]
public void Initialize()
{
//创建模拟数据
var dataContext = new FakeDataContext();
dataContext.Insert(new Message(1, null, null, "Robert", "Welcome to the MVC forums!", "body1"));
dataContext.Insert(new Message(2, 1, 1, "Stephen", "RE:Welcome to the MVC forums!", "body2"));
dataContext.Insert(new Message(3, 1, 2, "Robert", "RE:Welcome to the MVC forums!", "body3"));
dataContext.Insert(new Message(4, null, null, "Mark", "Another message", "body4"));
dataContext.Insert(new Message(5, 4, 4, "Stephen", "Yet another message", "body5"));
dataContext.Insert(new Message(6, 4, 5, "Jane", "Yet another message", "body6"));
//返回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);
}
/// <summary>
///验证Thread(1)动作将返回3条消息,而且所有的消息都有一个
///值为1的Id或者ParentThreadId属性
/// </summary>
[TestMethod]
public void ThreadReturnsMessagesInThread()
{
// Arrange
var controller = new ForumController(_repository);
// Act
var result = controller.Thread(1) as ViewResult;
// Assert
var model = result.ViewData.Model as List<Message>;
Assert.AreEqual(3, model.Count); //第一个线程中的3条消息
foreach (var m in model)
{
Assert.IsTrue(m.Id == 1 || m.ParentThreadId == 1);
}
}
[TestMethod]
public void NewThreadIncrementsThreadCount()
{
// Arrange
var controller = new ForumController(_repository);
var originalThreadCount = _repository.SelectThreads().Count;
// Act
var form = new FormCollection();
form.Add("author", "Stephen");
form.Add("subject", "New Thread!");
form.Add("body", "Body of new thread");
var result = controller.Create(form);
// Assert
var newThreadCount = _repository.SelectThreads().Count;
Assert.AreEqual(originalThreadCount + 1, newThreadCount);
}
[TestMethod]
public void NewReplyIncrementsMessageCount()
{
// Arrange
var controller = new ForumController(_repository);
var originalMessageCount = _repository.SelectMessages(1).Count;
// Act
var form = new FormCollection();
form.Add("parentThreadId", "1");
form.Add("parentMessageId", "1");
form.Add("author", "Stephen");
form.Add("subject", "New Thread!");
form.Add("body", "Body of new thread");
var result = controller.Create(form);
// Assert
var newMessageCount = _repository.SelectMessages(1).Count;
Assert.AreEqual(originalMessageCount + 1, newMessageCount);
}
[TestMethod]
public void NewReplyDoesNotIncrementsThreadCount()
{
// Arrange
var controller = new ForumController(_repository);
var originalThreadCount = _repository.SelectThreads().Count;
// Act
var form = new FormCollection();
form.Add("parentThreadId", "1");
form.Add("parentMessageId", "1");
form.Add("author", "Stephen");
form.Add("subject", "New Thread!");
form.Add("body", "Body of new thread");
var result = controller.Create(form);
// Assert
var newThreadCount = _repository.SelectThreads().Count;
Assert.AreEqual(originalThreadCount, newThreadCount);
}
}
}
列表1中的测试类包含下列单元测试:
? IndexReturnsMessageThreads()—验证Index()动作将返回2条ParentThreadId属性为NULL的消息;
? ThreadReturnsMessagesInThread()—验证Thread()动作将返回一个指定线程中的所有消息;
? NewThreadIncrementsThreadCount()—验证发送一条新的没有提供ParentThreadId属性的消息将使消息线程数加1;
? NewReplyIncrementsMessageCount()—验证把一条应答消息发送到一个线程将使该线程中的消息数加1;
? NewReplyDoesNotIncrementThreadCount()—验证发送一条应答消息不会增加总线程的数目。
上面这些单元测试应该是非常易于理解的。它们都利用了虚构的DataContext类。通过这种方式,数据库本身是不会被实际上使用或修改的。
此外还请注意,这些单元测试本身并不会与DataContext类直接交互。而是,这些单元测试仅仅与ForumRepository类交互。通过这种方式,如果我们决定改变我们的数据访问技术—例如,我们把LINQ to SQL改变为NHibernate—那么,我们不需要改变我们的单元测试。如果我们改变数据访问技术,那么,我们仅仅需要改变Initialize()方法。
三、 修改论坛Repository
为了满足新的单元测试,我不得不在ForumRepository类中添加两个新的方法。幸好,因为我们使用的是LINQ to SQL,所以实现该新的方法仅要求较少的代码。列表2给出了该新版本的ForumRepository类。
列表2–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.ParentThreadId == null
select m;
return threads.ToList();
}
public IList<Message> SelectMessages(int threadId)
{
var messages = _dataContext.GetTable<Message>();
var threads = from m in messages
where (m.Id == threadId || m.ParentThreadId == threadId)
select m;
return threads.ToList();
}
public Message AddMessage(Message messageToAdd)
{
_dataContext.Insert(messageToAdd);
return messageToAdd;
}
}
}
该SelectMessages()方法返回一个对应于一个特定线程的消息集合。AddMessage()方法把一个新的消息添加到数据库中。当添加一个消息(它启动一个新的线程)或在一个现有线程上添加一个应答时调用这个方法。
四、 修改Forum控制器
Forum控制器,类似于ForumRepository,还提供了两个新的方法。Forum控制器的修改版本包含于列表3中。
列表3–Controllers\ForumController.cs
using System.Web.Mvc;
using MvcForums.Models;
using Microsoft.Web.Mvc;
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");
}
public ActionResult Create(FormCollection form)
{
//创建新的消息
var messageToCreate = new Message();
if (!String.IsNullOrEmpty(form["parentThreadId"]))
messageToCreate.ParentThreadId = int.Parse(form["parentThreadId"]);
if (!String.IsNullOrEmpty(form["parentMessageId"]))
messageToCreate.ParentMessageId = int.Parse(form["parentMessageId"]);
messageToCreate.Author = form["author"];
messageToCreate.Subject = form["subject"];
messageToCreate.Body = form["body"];
messageToCreate.EntryDate = DateTime.Now;
//添加到数据库
_repository.AddMessage(messageToCreate);
//重定向
return RedirectToAction("Index");
}
public ActionResult Thread(int threadId)
{
ViewData.Model = _repository.SelectMessages(threadId);
return View("Thread");
}
}
}
该Create()动作负责在数据库中创建一个新的消息。这个动作使用了一个传递到此动作的form参数并且生成Message类的一个实例。然后,借助于该ForumRepository.AddMessage()方法,该消息类被添加到数据库中。
此外,Thread()动作返回一个相应于一个特定线程的消息的集合。这个动作把它的所有工作代理到ForumRepository.SelectMessages()方法。
五、 成功通过单元测试
在改变了ForumRepository和ForumController类之后,我们的新的单元测试成功通过!现在,我们的论坛应用程序共有5个单元测试
六、 创建视图
严格地说,目前为止,还不存在什么真正的为我们的论坛应用程序创建视图的任何理由。我们不需要实际地运行应用程序来确保应用程序正常工作。执行我们的单元测试可以比仅用肉眼观察能够提供更有力的证明证实应用程序正确工作。
然而,我也是一个人。我也喜欢偶尔运行一下应用程序,仅仅是为了达到检查程序是否能够正常运行的目的。因此,我创建了两个相应于Index()动作和Thread()动作的简单的视图。
其中,Index()动作相应的视图包含于列表4中。这个视图简单地以一个列表的方式显示所有的线程
列表4—Views\Forum\Index.aspx
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<div>
<ul>
<% foreach (var m in ViewData.Model)
{ %>
<li>
<%= Html.Encode(m.Author) %>
<%= Html.ActionLink(m.Subject, "Thread", new {threadId=m.Id})%>
</li>
<% } %>
</ul>
</div>
</body>
</html>
Index视图实现针对每一个线程生成一个超级链接。如果你点击一个超级链接,你便被导航到线程视图。列表5给出了这个线程视图的完整代码,此视图负责在一个特定的线程中显示所有的信息。
列表5—Views\Forum\Thread.aspx
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title></title>
</head>
<body>
<div>
<ul>
<% foreach (var m in ViewData.Model)
{ %>
<li>
<%= Html.Encode(m.Author) %>
<%= Html.Encode(m.Subject) %>
<p>
<%= Html.Encode(m.Body) %>
</p>
</li>
<% } %>
</ul>
</div>
</body>
</html>
请注意,Index和Thread这两个视图都是强类型化的。如果你观察一下无论哪一个视图的code-behind文件,你都会注意到这些视图都会以强类型化的方式把ViewData.Model属性转换成一个List<Movie>集合的实例。例如,下面的列表6就提供了Index视图对应的code-behind文件定义。
列表6—Views\Forums\index.aspx.cs
using System.Web.Mvc;
using MvcForums.Models;
namespace MvcForums.Views.Forum
{
public partial class Index : ViewPage<List<Message>>
{
}
}
七、 一点思考
当你阅读测试驱动开发方面的教程和书籍的时候,它们总是主张你无论如何不要偏离如下的编写步骤:
(1)编写测试
(2)满足测试
(3)重构
根据我的经验,程序不相当当做纯粹的。当处理论坛应用程序的时候,我总是先从编写测试开始。然而,有时当我开始编码时,我发现,为了满足测试要求,我不得不先进行一些相当特别的假定。
我让测试指导我接下来需要编写的代码。我把测试作为我需要满足的需求的表达。然而,在我写满足测试的编码过程中,有时发现自己常常被测试所表现的需求所困扰。于是乎,我需要重新考虑应用程序的设计。就是在这样的情形中,我最后不得不重新测试。
首先,我不认为使用驱动方式编程存在任何的错误。当你在设计的过程中发现存在瑕疵的时候,必须重新考虑改进应用程序的设计,即使是当开始编代码的时候。最后,我的结论是,使用测试驱动开发绝对不是你想像中的直线式行进方式编程。
八、 小结
在本篇中,我进一步修正了本论坛应用程序,从而使其进一步支持发布新消息及新的答复。此外,我们还新创建了四个新的单位测试驾驶我们的发展。
在下一篇中,我们需要探讨校验的议题。在把表单数据插入到服务器端数据库之前,我们必须对寄到服务器端控制器行动的数据校验进行校验。同时,我们也需要在我们的视图中显示相关的错误信息。