西北狼

-- 学而时习之,不亦乐乎!
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

MVC学习之依赖注入

Posted on 2009-06-01 10:59  西北老狼  阅读(527)  评论(0编辑  收藏  举报
依赖注入(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方法(包括列表、分页、详细信息、创建、更新和删除等等),而不需要连接真实的数据库。