一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28
本文根据《Professional ASP.NET MVC 1.0》中微软牛人Scott Guthrie 提供免费下载的第一章,一步一步演示如何通过ASP.NET MVC 1.0 正式版创建NerdDinner 范例程序。对了解如何使用最新的ASP.NET MVC 1.0框架创建Web Application 非常有帮助。本文由
http://forum.entlib.com 开源论坛小组提供。
本文继续学习之旅,一步一步通过ASP.NET MVC 1.0 实现NerdDinner 范例程序。
依赖注入(Dependency Injection)
现在DinnersController紧耦合DinnerRepository类,耦合(Coupling)指一个类显式依赖另外的一个类才能工作。
public class DinnersController : Controller
{
DinnerRepository dinnerRepository = new DinnerRepository();
//
// GET: /Dinners/Details/2
public ActionResult Details(int id)
{
Dinner dinner = dinnerRepository.GetDinner(id);
if (dinner == null)
return View("NotFound");
else
return View("Details", dinner);
}
}
因为DinnerRepository 类需要访问数据库,DinnersController类对DinnerRepository类的紧耦合导致DinnersController action方法的测试都需要连接数据库。
我们可以通过Dependency Injection(依赖注入)设计模式来解决这一问题,类之间(如Repository类提供数据访问)不再创建隐式的依赖。而是,通过调用方的构造函数的参数,显式传递依赖关系。如果依赖关系通过接口来定义,我们就可以针对单元测试的情况,灵活传递虚假(Fake)的依赖实现。这样,我们在创建测试相关的依赖实现时,不必访问真实的数据库。
下面演示具体实现,首先对DinnersController实现依赖注入。
提取IDinnerRepository接口
第一步是创建新的IDinnerRepository接口,封装Controller检索和更新Dinners对象所需要的Repository契约。
右键点击\Models文件夹,选择 Add->New Item菜单项,创建一个新的接口IDinnerRepository.cs。
另外一种方法是,使用Visual Studio 内置的重构工具(Refactoring tools),从现有的DinnerRepository类中自动提取并创建一个接口文件。如通过VS 提取这一接口文件,只需将光标定位到DinnerRepository 类中,右键并选择Refactor -> Extract Interface 菜单项:
随后,将弹出Extract Interface 对话框,接口命名默认为IDinnerRepository,并自动选择DinnerRepository类中的所有公共的方法,添加到接口中:
在点击OK按钮后,Visual Studio 将添加一个新的IDinnerRepository接口到应用程序中:
public interface IDinnerRepository
{
void Add(Dinner dinner);
void Delete(Dinner dinner);
System.Linq.IQueryable<Dinner> FindAllDinners();
System.Linq.IQueryable<Dinner> FindUpcomingDinners();
Dinner GetDinner(int id);
void Save();
}
现有的DinnerRepository 类将更新为实现该接口:
public class DinnerRepository : IDinnerRepository {
...
}
更新DinnersController支持构造器注入
现在实现新的接口,更新DinnersController类。
目前,DinnersController 类是硬编码的,如dinnerRepository属性总是类型为DinnerRepository 实例:
public class DinnersController : Controller {
DinnerRepository dinnerRepository = new DinnerRepository();
...
}
我们更改上述代码,将dinnerRepository 属性由DinnerRespository 类型更改为 IDinnerRepository接口类型,接着添加2个公共的DinnersController构造器。其中一个构造器允许传入IDinnerRepository 类型的参数,另外一个是默认的构造器,使用现有的DinnerRepository的实现:
public class DinnersController : Controller {
IDinnerRepository dinnerRepository;
public DinnersController()
: this(new DinnerRepository()) {
}
public DinnersController(IDinnerRepository repository) {
dinnerRepository = repository;
}
...
}
因为默认情况下ASP.NET MVC使用默认构造器创建控制器Controller类,DinnersController控制器在运行时将继续使用DinnerRepository类执行数据访问。
但是,现在我们可以更新单元测试代码,使用带参数的构造器,传入一个虚假的Dinner Repository的实现。虚假的Dinner repository 不需要访问真实的数据库,而是使用内存中的样本数据。
创建 FakeDinnerRepository 类
下面开始创建FakeDinnerRepository类。
首先,在 NerdDinner.Tests项目中创建Fakes目录,接着添加一个新的FakeDinnerRepository类到该目录(右键点击该目录,选择Add->New Class菜单项)。
更新FakeDinnerRepository 类,实现IDinnerRepository 接口。接着右键点击,并选择Implement interface IDinnerRepository 上下文菜单项:
这样,Visual Studio 将自动添加IDinnerRepository 接口成员到FakeDinnerRepository 类中,并附有默认的基础(存根)实现:
public class FakeDinnerRepository : IDinnerRepository
{
#region IDinnerRepository Members
public void Add(Dinner dinner)
{
throw new NotImplementedException();
}
public void Delete(Dinner dinner)
{
throw new NotImplementedException();
}
public IQueryable<Dinner> FindAllDinners()
{
throw new NotImplementedException();
}
public IQueryable<Dinner> FindUpcomingDinners()
{
throw new NotImplementedException();
}
public Dinner GetDinner(int id)
{
throw new NotImplementedException();
}
public void Save()
{
throw new NotImplementedException();
}
#endregion
}
接着更新FakeDinnerRepository 的实现代码,对作为构造函数参数传入的List<Dinner>集合进行访问,而不是真实的数据库记录:
public class FakeDinnerRepository : IDinnerRepository
{
private List<Dinner> dinnerList;
public FakeDinnerRepository(List<Dinner> dinners)
{
dinnerList = dinners;
}
public void Add(Dinner dinner)
{
dinnerList.Add(dinner);
}
public void Delete(Dinner dinner)
{
dinnerList.Remove(dinner);
}
public IQueryable<Dinner> FindAllDinners()
{
return dinnerList.AsQueryable();
}
public IQueryable<Dinner> FindUpcomingDinners()
{
return (from dinner in dinnerList
where dinner.EventDate > DateTime.Now
select dinner).AsQueryable();
}
public Dinner GetDinner(int id)
{
return dinnerList.SingleOrDefault(d => d.DinnerID == id);
}
public void Save()
{
foreach (Dinner dinner in dinnerList)
{
if (!dinner.IsValid)
throw new ApplicationException("Rule violations");
}
}
}
现在,虚假的IDinnerRepository 的实现不需要数据库了,可以工作在内存中的Dinner对象列表。
在单元测试中使用FakeDinnerRepository
我们回到DinnersController单元测试,之前由于数据库不能访问,而有异常或失败。在DinnersController类中,我们将使用填充了内存中范例Dinner数据的FakeDinnerRepository 类,来更新测试方法。示例代码如下:
///<summary>
/// Summary description for DinnersControllerTest
///</summary>
[TestClass]
public class DinnersControllerTest
{
List<Dinner> CreateTestDinners()
{
List<Dinner> dinners = new List<Dinner>();
for (int i = 0; i < 101; i++)
{
Dinner sampleDinner = new Dinner()
{
DinnerID = i,
Title = "EntLib.com 欢迎你",
HostedBy = "EntLib.com",
Address = "http://blog.EntLib.com",
Country = "中国",
ContactPhone = "12345678",
Description = "Some description",
EventDate = DateTime.Now.AddDays(i),
Latitude = 99,
Longitude = -99
};
dinners.Add(sampleDinner);
}
return dinners;
}
DinnersController CreateDinnersController()
{
var repository = new FakeDinnerRepository(CreateTestDinners());
return new DinnersController(repository);
}
[TestMethod]
public void DetailsAction_Should_Return_View_For_ExistingDinner()
{
// Arrange
var controller = new DinnersController();
// Act
var result = controller.Details(2) as ViewResult;
// Assert
Assert.IsInstanceOfType(result, typeof(ViewResult));
}
[TestMethod]
public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner()
{
// Arrange
var controller = new DinnersController();
// Act
var result = controller.Details(999) as ViewResult;
// Assert
Assert.AreEqual("NotFound", result.ViewName);
}
}
注意:上述类需要引用如下的namespace:
using NerdDinner.Controllers;
using NerdDinner.Models;
using NerdDinner.Tests.Fakes;
现在我们运行这些测试方法时,均验证通过:
最大的好处是,运行这些测试仅仅需要不到1秒,并且不需要任何复杂的安装/清理逻辑。现在,我们可以单元测试DinnersController类中的所有action方法(包括列表、分页、详细信息、创建、更新和删除等等),而不需要连接真实的数据库。
Repository Pattern 的英文解释:
A Repository mediates between the domain and data mapping layers, acting like an in-memory domain object collection. Client objects construct query specifications declaratively and submit them to Repository for satisfaction. Objects can be added to and removed from the Repository, as they can from a simple collection of objects, and the mapping code encapsulated by the Repository will carry out the appropriate operations behind the scenes. Conceptually, a Repository encapsulates the set of objects persisted in a data store and the operations performed over them, providing a more object-oriented view of the persistence layer. Repository also supports the objective of achieving a clean separation and one-way dependency between the domain and data mapping layers.
原文:http://martinfowler.com/eaaCatalog/repository.html
下面我简单翻译了一下,仅供参考:
Repository 是位于业务领域层和数据映射层之间协调者,就像一个内存中的业务对象集合。客户端对象清楚地构造查询语句,并提交给Repository执行。业务对象可以添加或从Repository中删除,就像对一个简单集合对象的操作一样,Repository封装的映射代码负责执行后台的正确操作。
从概念上而言,Repository 封装了持久化业务对象到数据库,以及针对Repository的操作方法,提供了持久层更加面向对象的视图。Repository 也支持实现业务领域层和数据映射层之间的清晰隔离和单向依赖。
关于Dependency Injection(依赖注入),可以参考学习Enterprise Library 小组的Unity Application Block: