依赖注入(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方法(包括列表、分页、详细信息、创建、更新和删除等等),而不需要连接真实的数据库。
来自西北的狼!