其他模板很适合于试验或创建概念证明,但它们通常会包含许多会让人分神且在真正的企业应用程序中不必要的干扰内容。
对于 ASP.NET MVC 应用程序开发,我建议使用在图 1 和图 2 中阐释的方法,其中包含以下项目:
- 该层只能访问 Controllers、Service、Domain 和 Shared 项目。
- 该层与 Service、Domain 和 Shared 项目通信。
- 该层与 DataAccess、Domain 和 Shared 项目通信。
- 该层与 Domain 和 Shared 项目通信。
- Domain 项目包含应用程序使用的域项目,并且禁止与任何项目通信。
- 图 1 各层之间的交互
图 2 解决方案结构示例
结果就是您的 Web 项目仅包含真正与 UI 相关的代码。
在单个解决方案文件夹中定位您的所有测试项目和测试将会显著减少默认解决方案资源管理器视图中的干扰内容,从而允许您轻松地定位您的测试。
若要按测试类型对测试进行分组,请在 Tests 解决方案文件夹内为您计划编写的每种测试类型都创建一个文件夹。
图 3 显示了一个 Tests 解决方案文件夹的示例,其中包含多个测试类型文件夹。
图 3 Tests 解决方案文件夹示例
图 4 显示具有测试项目的解决方案资源管理器。
图 4 解决方案资源管理器中的测试项目
介绍针对您的体系结构的依赖关系注入
但在您可以利用 Test Double 所提供的灵活性之前,必须对您的代码进行设计,以便处理依赖关系的注入。
使用类并不知道其任何依赖关系的实际具体实现,仅知道支持依赖关系的接口;具体实现由使用类或依赖关系注入框架提供。
通过松散耦合,您在撰写单元测试时可以轻松地替换您的依赖关系的 Test Double 实现。
有三种主要方法可用于实现依赖关系注入:
- 属性注入
- 构造函数注入
- 使用依赖关系注入框架/控制容器反转(自此以后称作 DI/IoC 框架)
此方法简单明了并且不需要工具。
图 5 属性注入
- // Employee Service
- public class EmployeeService : IEmployeeService {
- private ILoggingService _loggingService;
- public EmployeeService() {}
- public ILoggingService LoggingService { get; set; }
- public decimal CalculateSalary(long employeeId) {
- EnsureDependenciesSatisfied();
- _loggingService.LogDebug(string.Format(
- "Calculating Salary For Employee: {0}", employeeId));
- decimal output = 0;
- /*
- * Complex logic that needs to be performed
- * in order to determine the employee's salary
- */
- return output;
- }
- private void EnsureDependenciesSatisfied() {
- if (_loggingService == null)
- throw new InvalidOperationException(
- "Logging Service dependency must be satisfied!");
- }
- }
- }
- // Employee Controller (Consumer of Employee Service)
- public class EmployeeController : Controller {
- public ActionResult DisplaySalary(long id) {
- EmployeeService employeeService = new EmployeeService();
- employeeService.LoggingService = new LoggingService();
- decimal salary = employeeService.CalculateSalary(id);
- return View(salary);
- }
- }
最后,随着您的对象的依赖关系数目的增加,实例化对象所需的代码量也将增加。
此方法也简单明了,但与属性注入不同,您可以确保始终设置该类的依赖关系。
图 6 构造函数注入
- // Employee Service
- public class EmployeeService : IEmployeeService {
- private ILoggingService _loggingService;
- public EmployeeService(ILoggingService loggingService) {
- _loggingService = loggingService;
- }
- public decimal CalculateSalary(long employeeId) {
- _loggingService.LogDebug(string.Format(
- "Calculating Salary For Employee: {0}", employeeId));
- decimal output = 0;
- /*
- * Complex logic that needs to be performed
- * in order to determine the employee's salary
- */
- return output;
- }
- }
- // Consumer of Employee Service
- public class EmployeeController : Controller {
- public ActionResult DisplaySalary(long employeeId) {
- EmployeeService employeeService =
- new EmployeeService(new LoggingService());
- decimal salary = employeeService.CalculateSalary(employeeId);
- return View(salary);
- }
- }
较大的应用程序通常具有过多的依赖关系,以致无法通过对象的构造函数提供它们。
为了在本文中演示 DI/IoC 框架,我将使用 StructureMap。
利用 StructureMap 让依赖关系注入更上一层楼
您可以使用程序包管理器控制台 (Install-Package StructureMap) 或 NuGet 程序包管理器 GUI(右键单击您的项目的引用文件夹,然后选择“管理 NuGet 程序包”)通过 NuGet 来安装该框架。
您可以通过以下两种方法中的一种在 Global.asax 的 Application_Start 方法中配置依赖关系。
第一种方法是手动指示 StructureMap,对于特定的抽象实现,它应该使用特定的具体实现:
- ObjectFactory.Initialize(register => {
- register.For<ILoggingService>().Use<LoggingService>();
- register.For<IEmployeeService>().Use<EmployeeService>();
- });
此外,因为您在 ASP.NET MVC 站点的 Application_Start 中注册依赖关系,因此,您的 Web 层必须直接知道绑定有依赖关系的应用程序的其他每个层。
通过此方法,StructureMap 将扫描您的程序集,并且在它遇到某一接口时,会查找关联的具体实现(基于一个概念,即依据惯例,名为 IFoo 的方法将映射到具体实现 Foo):
- ObjectFactory.Initialize(registry => registry.Scan(x => {
- x.AssembliesFromApplicationBaseDirectory();
- x.WithDefaultConventions();
- }));
这是通过创建依赖关系解决程序并将其定位于 Shared 项目中来实现的(因为它将需要由具有依赖关系的所有应用程序层来访问):
- public static class Resolver {
- public static T GetConcreteInstanceOf<T>() {
- return ObjectFactory.GetInstance<T>();
- }
- }
该函数接受泛型参数 T,该参数表示为其查找具体实现的接口;并且返回 T,这是传入接口的实际实现。
但遗憾的是,它驻留在 System.Web.MVC DLL 中,而我不希望在应用程序体系结构的非 Web 层中具有对特定于 Web 技术的库的引用。
您需要完成的全部工作就是调用 Resolver 类的静态 GetConcreteInstanceOf 函数,并且将其传递给您在为其查找具体实现的接口,如图 7 中所示。
图 7 解析代码中的依赖关系
- public class EmployeeService : IEmployeeService {
- private ILoggingService _loggingService;
- public EmployeeService() {
- _loggingService =
- Resolver.GetConcreteInstanceOf<ILoggingService>();
- }
- public decimal CalculateSalary(long employeeId) {
- _loggingService.LogDebug(string.Format(
- "Calculating Salary For Employee: {0}", employeeId));
- decimal output = 0;
- /*
- * Complex logic that needs to be performed
- * in order to determine the employee's salary
- */
- return output;
- }
- }
它的具体情形是这样的:
- (您将会在图 7 中发现 EmployeeService 和 CalculateSalary 函数。)
- 有一个要求,即必须记录对 CalculateSalary 函数的所有调用。
- 调用日志记录服务当前会引发一个异常。
- 需要在针对日志记录服务的工作按计划开始前完成该任务。
在该项目中,我添加了一个 Fakes 文件夹,因为为了完成我的测试,我需要 ILoggingService 的虚设实现。
图 8 用于共享测试代码和虚设的项目
通常,这意味着它具有 void 方法的空实现,并且函数实现包含返回硬编码值的返回语句,如下所示:
- public class LoggingServiceFake : ILoggingService {
- public void LogError(string message, Exception ex) {}
- public void LogDebug(string message) {}
- public bool IsOnline() {
- return true;
- }
- }
开始时,我将在 TestDrivingMVC.Service.Test.Unit 单元测试项目中创建一个测试类,按照前面所述的命名约定,我将其命名为 EmployeeServiceTest,如图 9 中所示。
图 9 EmployeeServiceTest 测试类
- [TestClass]
- public class EmployeeServiceTest {
- private ILoggingService _loggingServiceFake;
- private IEmployeeService _employeeService;
- [TestInitialize]
- public void TestSetup() {
- _loggingServiceFake = new LoggingServiceFake();
- ObjectFactory.Initialize(x =>
- x.For<ILoggingService>().Use(_loggingServiceFake));
- _employeeService = new EmployeeService();
- }
- [TestMethod]
- public void CalculateSalary_ShouldReturn_Decimal() {
- // Arrange
- long employeeId = 12345;
- // Act
- var result =
- _employeeService.CalculateSalary(employeeId);
- // Assert
- result.ShouldBeType<decimal>();
- }
- }
您要特别注意的代码行是:
- ObjectFactory.Initialize(x =>
- x.For<ILoggingService>().Use(
- _loggingService));
我将此代码放置于用 TestInitialize 标记的方法中,这指示单元测试框架在测试类中运行每个测试前都执行该方法。
这样做使我能够在不受到日志记录服务状态的影响下完成编码和单元测试,并且编写不依赖于任何依赖关系的真正的单元测试代码。
通过创建从 DefaultControllerFactory 继承的类(参见图 10),您可以控制创建控制器的方式。
图 10 自定义控制器工厂
- public class ControllerFactory : DefaultControllerFactory {
- private const string ControllerNotFound =
- "The controller for path '{0}' could not be found or it does not implement IController.";
- private const string NotAController = "Type requested is not a controller: {0}";
- private const string UnableToResolveController =
- "Unable to resolve controller: {0}";
- public ControllerFactory() {
- Container = ObjectFactory.Container;
- }
- public IContainer Container { get; set; }
- protected override IController GetControllerInstance(
- RequestContext context, Type controllerType) {
- IController controller;
- if (controllerType == null)
- throw new HttpException(404, String.Format(ControllerNotFound,
- context.HttpContext.Request.Path));
- if (!typeof (IController).IsAssignableFrom(controllerType))
- throw new ArgumentException(string.Format(NotAController,
- controllerType.Name), "controllerType");
- try {
- controller = Container.GetInstance(controllerType)
- as IController;
- }
- catch (Exception ex) {
- throw new InvalidOperationException(
- String.Format(UnableToResolveController,
- controllerType.Name), ex);
- }
- return controller;
- }
- }
在这个新的控制器工厂中,我具有一个公共的 StructureMap 容器属性,它基于 StructureMap ObjectFactory 获取集(在图 10 的 Global.asax 中配置)。
因为我在最初配置 StructureMap 时使用了 StructureMap 自动注册和扫描功能,所以无需执行任何其他操作。
当您为控制器声明参数化的构造函数时,将在新的控制器工厂中解析控制器时自动解析依赖关系。
这意味着您无需手动添加代码来解析控制器的依赖关系 — 但您仍可以按照前述内容来使用虚设。
图 11 解析控制器
- public class HomeController : Controller {
- private readonly IEmployeeService _employeeService;
- public HomeController(IEmployeeService employeeService) {
- _employeeService = employeeService;
- }
- public ActionResult Index() {
- return View();
- }
- public ActionResult DisplaySalary(long id) {
- decimal salary = _employeeService.CalculateSalary(id);
- return View(salary);
- }
- }
通过在您的 ASP.NET MVC 应用程序中使用这些实践和技术,整个 TDD 过程将更加轻松和简明。