一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28


一步一步学习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方法(包括列表、分页、详细信息、创建、更新和删除等等),而不需要连接真实的数据库。
 
http://forum.EntLib.com 开源小组备注:NerdDinner 范例程序中使用了Repository Pattern模式,下面进行一些简单的描述,欢迎交流。
 
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:
 

 

发表 @ 2009年4月15日 21:37

打 印

评论

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by silenus-G at 2009/4/30 11:13
Gravatar
---------------------------------------------------------------------------------
public DinnersController(IDinnerRepository repository) {
dinnerRepository = repository;
}
-----------------------------------------------------------------------
这个地方是IDinnerRepository 还是DinnerRepository??
顺便解释一下这里和IDinnerRepository这个接口可以吗,谢谢!!

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by entlibforum at 2009/4/30 11:35
Gravatar
silenus-G,是IDinnerRepository 接口类型。

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by silenus-G at 2009/4/30 14:56
Gravatar
恩,tks。我知道自己哪写错了!

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by Nick at 2009/5/28 14:52
Gravatar
最后的那里的两个测试方法

[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);
}

都是 var controller = CreateDinnersController();

谢谢

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by 蔚蓝海 at 2009/6/25 13:15
Gravatar
IDinnerRepository dinnerRepository;

public DinnersController()
: this(new DinnerRepository()) {

}
public DinnersController(IDinnerRepository repository) {
dinnerRepository = repository;
}

这个有错误 可以解释下怎么回事吗

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by entlib at 2009/6/28 19:50
Gravatar
蔚蓝海, 到论坛http://forum.entlib.com 下载源码,对比分析一下就知道了。

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by chenchen at 2009/9/8 16:12
Gravatar
写这么多代码为了一个测试,是否值得,等于是要把数据操作的那些方法都虚拟出来

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by entlibforum at 2009/9/8 20:24
Gravatar
chenchen,

呵呵,我也认为没有这种必要。

# re: 一步一步学习ASP.NET MVC 1.0创建NerdDinner 范例程序,Part 28

Left by Yinner at 2010/1/26 22:34
Gravatar
在public class DinnersControllerTest中并没有显式调用
DinnersController CreateDinnersController()所创建的dinners,
结果是怎么就用上了呢?

您的评论:



 (不显示)


 
 
 
Please add 6 and 1 and type the answer here:
    
 

评论预览窗口:

 
«九月»
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789