可测试 MVC
构建可测试 ASP.NET MVC 应用程序
Justin Etheredge
鏈 枃璁 ㄨ :
|
本文使用以下技术: ASP.NET |
内容
在 ASP.NET MVC 中测试支持
工具
应用程序方案
高级应用程序设计
项目模型的抽象化
知识库模式
插入知识库
实现服务层
隔离的逻辑视图
测试路由
将操作的依赖项作为参数传递
测试操作的结果
上的换行
您可能已经听说过,旧 adage"更好、 更快、 便宜,选择任意两个。" 如果希望,一些好和快速,它不要低成本,如果您希望快速而廉价的内容,它不是很好。便宜、 速度更快,和更好地意味着我们需要编写更多更快速、 右?如果只是简单。学习更快地键入可能满足该的要求的两个,但它不会进行任何更好地开发的软件。那么,如何使软件更好?"更好地"意味着什么?
"最好"产生灵活、 可维护软件缺陷 ; 低数的方法更好的软件是长期的可维护性。要实现此目的,关键设计决策是确保组件松散地耦合。松散耦合的软件具有许多优点。醒目的是它提高了我们测试解决方案的能力。如果我们编写的软件可以被分为很容易地小部分,它将成为易于测试。这听起来简单时您单词它这样,但软件的使用现在就是难以测试或维护不是简单我们可能希望显示。软件需要在执行任何操作有用,加以,但开发人员需要工具和技术来减少耦合,以便更轻松地测试解决方案。
在 ASP.NET MVC 中测试支持
我们发现一个 renaissance 测试工具和技术中的内容。您查看的任何位置您查找测试框架和工具 (如 mocking 容器和依赖关系注入框架。不是所有这些工具的目的是用于测试,但最后允许我们构建更具可测试性的应用程序。
从常规角度来看一个可测试的应用程序是松散的应用程序结合足以允许进行测试以隔离独立部分。编写可测试的应用程序需要大量与的开始设计工作,但生成可测试的应用程序,开发人员还必须使用工具和设计模式可轻松地测试的周围的框架。如果您不使用的工具,允许您构建可测试的应用程序,将帮助良好的设计不量。令人欣慰的是,ASP.NET MVC 旨在从一开始就设置是可测试,并几个关键设计选择是以允许开发人员能够创建可测试的应用程序。
某些会说模型视图控制器 (MVC) 模式本质上更可比页面控制器模式 (该图案所采用的 ASP.NET Web 窗体) 测试由于它促进的应用程序流逻辑和显示逻辑分离。在 MVC 模式中的控制流的应用程序逻辑驻留在该控制器类内使开发人员可以很容易地实例化并执行此逻辑,就好像它是其他任何.NET 类。这允许开发人员轻松地执行很多相同的方式它们将测试任何其他 POCO (普通的旧 CLR 对象) 类使用简单的单元测试业务逻辑。
另一选项所做的 Microsoft 开发小组以便为插入框架的许多部分。更重要的部分,这些控制器工厂,控制的控制器类实例化的。允许开发人员可以替换或扩展控制器工厂使得执行逻辑可解析和插入该控制器的依赖项。允许在运行时被注入控制器的依赖项是创建多个松散耦合和可测试的应用程序的关键。
在传统的 ASP.NET 的开发人员在测试过程中,障碍之一就是静态类,每个请求过程中使用的该文章。ASP.NET MVC 团队进行换行的许多.NET 静态帮助器类 (如 HttpContext 和 HttpRequest),以便它们可以与一个存根 (stub) 测试过程中替换决定。MVC ASP.NET 提供了许多帮助开发人员避免使用这些的类的抽象但在您是需要使用它们的位置包装方便此代码测试。
工具
尽管 MVC ASP.NET 提供了许多工具开发人员需要创建可测试的应用程序,您不能依赖它指导您正确方向。而是,必须设计应用程序有意支持测试,并这几个其他工具帮助:
- 单元测试框架。xUnit.NET张欣孙和 Jim Newkirk。xUnit 地运行自动化的单元测试。许多人使用 MSTest,但很多其他单元测试框架有出,并我鼓励您看一看其中的一部分。我选择 xUnit.NET,因为它是简单,轻松扩展,并语法非常干净。我 Resharper,为使用该 xUnit 测试运行程序,但在 GUI xUnit 测试运行程序处于示例应用程序的工具文件夹。
- 依赖关系注入框架。Ninject 2通过 Nate Kohari。Ninject 地绑定应用程序中的类。我将介绍此方法在本文后面更有深度。
- 模拟框架。通过 Clarius 咨询 Moq. Moq 提供了一个框架,用于测试过程中模拟接口和类。
应用程序方案
若要向如何设计一个可测试应用程序,我将使用一个简单的应用程序方案的列表中的类别,并在网页上显示产品。应用程序还包括简单的屏幕,可以查看、 编辑,和创建类别和产品。图 1 中, 所示,简单架构由类别和产品之间的一对多映射组成。
图 1 示例应用程序的架构 (单击图像可查看大图)
高级应用程序设计
最初的设计时创建的应用程序,可以转长方式向帮助或阻碍应用程序的长期运行状况,其可维护性和可测试性。许多体系结构中,方法是没有创建过多的系统开销的情况下查找抽象的最佳数量。MVC 模式已经提供了通过定义三层应用程序的一些体系结构指南。某些开发人员可能会认为这些三个级别提供足够的抽象生成较大的应用程序。遗憾的是,这通常并不是这种情况,正如您将看到模型可以轻松地为多个单个层。
您必须请记住在许多小型应用程序,包括本文中,该示例中三个抽象级别的是可能足够。在这些情况下,某些视图、 控制器,和进行交互的类的一组是可能不足。但是,创建极具可测试性的应用程序需要多个抽象的其他层。在我们的处置的许多体系结构模式可以帮助形成包括 MVC 模式将总体设计。
MVC ASP.NET 代码隐藏页
如果您是下列 最终发行版之前的 ASP.NET MVC,您可能已经注意到在代码隐藏页从删除了框架。这些页不提供 ASP.NET MVC 框架中的任何功能,并提升置于逻辑视图,不属于。在视图中包含的任何逻辑很难很多相同的方式在 ASP.NET Web 窗体中的代码隐藏文件难以测试的测试。
阅读此文章的最可能常见使用模式,但您可能不具有考虑其含义为应用程序可测试性。MVC 模式的第一部分是包含到客户端呈现数据的逻辑视图。最初,这是一个用户界面但是客户端 Web 浏览器、 Web 服务、 客户端 JavaScript 和等等。视图应能用于仅一个使用者,呈现数据,所需的逻辑的数量应被提取到尽可能多的帮助器类。
该模型表示应用程序的后端。该应用程序非常松散定义 ASP.NET MVC 模式的实现中的此部分可以在许多不同的窗体上并且它将很有可能依赖您的应用程序的范围。在小一些复杂的业务逻辑与应用程序,渚嬪的方式 此层可能只是一组的业务对象使用您控制器层中的直接交互的活动记录模式。
控制器编排应用程序的流程,通过采用该模型中的数据,并将它传递给适当的视图。由于此类是显示逻辑分开,使用几种技巧应该由您在 ASP.NET 运行库上实例化此类单元测试中的并没有依赖项。这将使完成测试的控制器而不必运行在 Web 服务器。
项目模型的抽象化
当查看这些层可能会看到几个位置的其他抽象会更容易地测试应用程序。这是通常使用更复杂应用程序的控制器和模型之间的一个区域。如果分层我先前介绍您控制器的操作可能类似下面:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formCollection)
{
Category category = Category.FindById(id);
category.Name = formCollection["name"];
category.Save();
return RedirectToAction("List");
}
挑战此处至少有双重。 第一次,无法轻松地测试此代码而无需一个数据库连接。 因为我们使用活动记录模式,我们不能很容易地交换出数据访问代码被耦合到此域实体。 此外,如果业务逻辑不完全包含在域实体中您可以启动泄漏的业务或应用程序逻辑到控制器类。 这可能导致重复的代码或甚至更糟糕,由不一致的实现中使用而引起的错误。 因为您现在需要确保您正在测试相同的行为在多个应用程序中的位置,这可以影响可测试性。
知识库模式
下一步中提高这种设计,可测试性是利用在他簿企业应用程序结构的图案中详细介绍马丁 Fowler 通过知识库模式。 知识库模式被错误的通常解释为位于应用程序和数据库,之间的但实际上它位于您的应用程序和任何种类的永久存储区之间的层。 此模式将限制到几个键的位置,使您可以删除它,以便于在隔离的测试应用程序逻辑的所有数据访问。
知识库模式的实现提供显示为一个在内存中的知识库您永久存储的视图。 可能类似于下面这样一个储存库接口:
public interface ICategoryRepository
{
IEnumerable<Category> FindAll();
void Insert(Category category);
Category FindById(int id);
void Update(Category category);
}
在隔离测试
有些人可能会 认为在隔离测试并不重要,并选择只编写集成测试。 如 MSTest 或 NUnit 测试框架仍然可以执行集成的测试,但测试多层应用程序包括访问磁盘保留数据到一个的数据库,在网络中进行调用,并且等等。 这类测试非常重要应该在所有的应用程序中存在但应用程序随着这些测试变得非常慢,并且可以脆弱在不同的开发人员的计算机上。 分隔单元测试和集成测试可以运行单元测试快速可靠地在开发计算机上定期生成服务器上运行的集成测试时。
当您开始分开另一个和在隔离的测试的类时,您可以找到该测试将成为脆弱少得多和要容易得多。 模拟一个依赖项,您完全什么它返回,您控制,您可以更轻松地测试调用类的情况下边缘。 此外,因为您不能依赖于多级别的依赖项,不必您为您测试的方案做准备时尽可能考虑到。 请记住在隔离测试类不能替代编写集成测试,以确保所有组件的很好地协同都工作。
此处需注意重要的是您正在移动该数据持久性出域实体,您可以更轻松地替换在测试过程中一个单独类。 对前面所示的编辑操作模式将类似于 图 2 。
插入知识库
图 2 中,代码看起来骞插噣,但是如何将替换库类测试过程中? 这很容易。 传递的而不是实例化此类在方法内,给它在类中。 在这种情况下可以作为构造函数参数传递它。 出现的问题,但是,是您不控制控制器类的实例化。 若要解决此问题,您可以使用前面所引用的依赖关系注入框架。 市场上有大量的框架。 选择了使用 Ninject 2。 (如果不熟悉依赖关系注入请参阅相应的文章在 2005 年 9 月的 MSDN Magazine 通过 Griffin Caprio" 依赖关系注入." 正如我,所说的第一步中使用依赖关系注入将是传递到控制器的该 CategoryRepository 通过控制器的构造函数:
public CategoryController(ICategoryRepository categoryRepository)
{
this.categoryRespository = categoryRepository;
}
图 2 利用存储库
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection formCollection)
{
var categoryRepository = new CategoryRepository();
Category category = categoryRepository.FindById(id);
category.Name = formCollection["name"];
categoryRepository.Update(category);
return RedirectToAction("List");
}
下一步中,您需要告诉 Ninject 它看到 ICategoryService 接口时, 需要插入该 CategoryService 的实例。 每个依赖关系注入框架具有配置的不同方法并用 Ninject 基于代码的配置而不是更常见的 XML。 非常简单此绑定是代码以实现它将是一个行:
Bind<ICategoryRepository>().To<CategoryRepository>().InTransientScope();
此语句指示要绑定到 CategoryRepository 意味着每次请求一个 ICategoryRepository,您获取 CategoryRepository 的新实例一个临时范围内的 ICategoryRepository 依赖关系注入框架。 这是在不同类似于您继续得到储备库的同一实例的一个单独作用域。
与控制器重构,以便在构造函数传递储备库,现在可以测试数据库的独立于控制器的行为。 一种方法实现此目的是通过编写一个虚拟类 (通常称为一个存根 (stub)) 实现 ICategoryRepository 接口。 然后可以实现的方法需要返回虚拟数据并执行虚拟的操作虽然这看起来很多工作。 鍙 ﹀ 的方式 时会怎么样您需要在不同的测试中返回多个组的单个方法虚拟数据? 您将得到与多个实现的存根 (stub),该存根 (stub) 中的标志,并且可能会大量的额外工作。
下面是其中一个很好的 mocking 框架可以进入。 一个 mocking 框架允许您创建模拟对象。 模拟对象允许开发人员模拟通过接口或虚拟的方法测试过程中的另一个类的行为,然后验证发生了预期的事件。 模拟对象 (也只是称为模拟) 的行为是什么使您可以替换您的类中的依赖项和然后而真实的依赖项就地测试这些类。
这可能有点令人困惑,但需要让您执行某些 stubbing mocking 框架。 是的右侧。 由于模拟可以断言已它,执行某个操作,将存根 (stub) 提供虚拟数据或操作的对象,它应该是清除在大多数实际情况下您的这两个的混合。 提供此事实,最 mocking 框架有允许您没有声明任何行为存根出方法或属性的功能。 (如果您仍是有点困惑与存根 (stub) 模拟,马丁 Fowler 有一种很好文章上该的主题" 模拟不存根 (stub)."
图 3 中的该代码显示了一个测试,mocks ICategoryRepository 接口存根 FindAll 方法,以便返回虚拟列表类别,然后将该 mocked 的实例传递给控制器类。 然后将列表在控制器类上调用方法,则可以将断言。 在这种情况下测试断言该模型的 IEnumerable 的类别并有一个类别列表中。
图 3 使用模拟的列表操作测试
[Fact]
public void ListActionReturnsListOfCategories()
{
// Arrange
// create the mock
var mockRepository = new Mock<ICategoryRepository>();
// create a list of categories to return
var categories = new[] { new Category {Id = 1, Name = "test"} };
// tell the mock that when FindAll is called,
// return the list of categories
mockRepository.Setup(cr => cr.FindAll()).Returns(categories);
// pass the mocked instance, not the mock itself, to the category
// controller using the Object property
var controller = new CategoryController(mockRepository.Object);
// Act
var result = (ViewResult) controller.List();
// Assert
var listCategories = Assert.IsAssignableFrom<IEnumerable<Category>>(result.ViewData.Model);
Assert.Equal(1, listCategories.Count());
}
在此时应用程序正在查找很好。 我们有一个库可以在测试和一个模拟该知识库,以便我们可以在隔离测试控制器行为的方法中被替换的。 某些的应用程序这可能是相对于控制器,必需的但很多中型到大型应用程序仍然有另一层抽象的。
细粒度的服务
在更复杂的应用程序中,您可能采用更精细的服务方法,并有一组任务或事件的类表示可以在系统中执行的操作。 这会产生双重影响。 第一次,它可以减少大小和您的服务类的复杂性。 这些类可以增长很大,如果它们不包含。 第二个,它提供了更多控制依赖项因为现在您只需要担心的单个操作依赖项而不是整个服务的依存关系。
实现服务层
服务层应用程序的域模型之上,并提供一组可对其执行的操作。 这使您可以集中属于您的应用程序,但一定可能不属于域模型内的逻辑的一个位置,将有否则可能会泄漏到控制器的方法的逻辑。 渚嬪的方式 如果需要进行安全检查, 图 2 中的代码周围吗? 不希望 (尽管您可能在某些情况下),操作方法中执行的操作因为重复此操作在另一个位置的使用需要将连同其安全性。 或者如果您想要将事务周围的操作吗? 您肯定不希望此逻辑控制器类中。
而,您可以提供一层的类,定义应用程序的服务,并且使用这些类来执行不同的操作。 在本文,示例如一个简单应用程序中服务层可能十分紧密地模仿知识库,该方法在下面的代码中可以看到,但如果应用程序有更多的业务逻辑,这些方法将可能代表业务操作而不是基本的 CRUD 方法:
public interface ICategoryService
{
IEnumerable<Category> FindAll();
Category FindById(int id);
void Save(Category category);
}
在服务中的前两个方法,调用委派到存储库,但结果返回然后您可能发现的服务有一个 Save 方法而更新和删除方法在存储库。 由于应用程序是能够以确定对象是否已被保存,决定调用 Update 或储备库上的插入可保留最多到服务。
服务层插入组合可极大地减少了知识库类在控制器类之间在耦合,同时保持较亮的控制器,因为您可以将大量的逻辑推入该服务。 控制器类的设计必须进行更改以反映的储存库,而不是服务的使用,而不是将库注入到这些类中,您插入该服务。 反过来,您在存储库注入服务。 这可能复杂,但依赖关系注入听起来不甚至发现 — — 它获取为您有线。
整个依赖关系注入配置 (包括知识库产品和服务) 的外观 图 4 .
图 4 依赖关系注入配置
public class DefaultModule: NinjectModule
{
public override void Load()
{
Bind<ICategoryService>().To<CategoryService>().InTransientScope();
Bind<ICategoryRepository>().To<CategoryRepository>().InTransientScope();
Bind<IProductService>().To<ProductService>().InTransientScope();
Bind<IProductRepository>().To<ProductRepository>().InTransientScope();
}
}
现在,我们的存储库和服务通过构造函数将传递需要构造使用依赖关系注入框架在控制器。 依赖关系注入框架处理结构和注入的依赖项,但您需要询问控制器类从框架。 这是更容易,而您可能希望由于 ASP.NET MVC 团队在控制器工厂可插入。 您只需要实现在 图 5 ,控制器工厂和可以然后很容易地替换控制器类的默认实例化。
图 5 Ninject 控制器工厂
public class NinjectControllerFactory : DefaultControllerFactory
{
private readonly IKernel kernel;
public NinjectControllerFactory(IKernel kernel)
{
this.kernel = kernel;
}
protected override IController GetControllerInstance(Type controllerType)
{
return (IController)kernel.Get(controllerType);
}
}
由于默认实现从继承的控制器工厂,您只需要重写 GetControllerInstance 方法并再请求 Ninject 内核控制 Ninject 中的对象实例化的类的类型。 当内核收到一个控制器类型时,Ninject self-binds (试图构造) 类型因为它是一个具体的类型。 如果内核接收 CategoryController 类型,构造函数将具有 ICategoryService 类型的参数。 Ninject 查找是否有这种类型的一个绑定,执行相同的操作时发现类型,寻找一个构造函数。 Ninject 内核实例化一个 CategoryRepository,并将它传递给构造函数,而对于 ControllerService。 然后它将传递 ControllerService 对象构造函数为该 CategoryController。 所有发生这种情况依赖关系注入容器内部只需通过请求该类型。
其工作,必须注册控制器工厂。 注册要求的只有一行 global.asax 文件:
public void Application_Start()
{
// get the Ninject kernel for resolving dependencies
kernel = CreateKernel();
// set controller factory for instantiating controller and injecting dependencies
ControllerBuilder.Current.SetControllerFactory(new NinjectController Factory(kernel));
}
现在,当用户请求一个 URL 以及 ASP.NET MVC 试图创建控制器,它实际上结束构造由依赖关系注入容器。 时测试控制器,您可以模拟隔离,在测试控制器服务,如 图 6 所示。
图 6 测试列表 Mocked 服务操作
[Fact]
public void ListActionReturnsListOfCategories()
{
// Arrange
var mockService = new Mock<ICategoryService>();
var categories = new[] { new Category { Id = 1, Name = "test" } };
mockService.Setup(cr => cr.FindAll()).Returns(categories);
var controller = new CategoryController(mockService.Object);
// Act
var result = (ViewResult) controller.List();
// Assert
var listCategories = Assert.IsAssignableFrom<IEnumerable<Category>>(result.ViewData.Model);
Assert.Equal(1, listCategories.Count());
}
隔离的逻辑视图
您可能会在此时认为您具有足够层,以便完成可测试性。 但是,我们不还研究过视图的复杂性。 假设在中所具有产品价格属性类。 (示例应用程序显示这)。 现在让我们假设您要向用户显示此价格格式化为货币。 如图所示某些开发人员可能会说您应该将此要求放在您的视图:
<%= Html.Encode(String.Format("{0:c}", this.Model.Price)) %>
我个人不喜欢这种方法,因为我不能字段在视图中被格式化时,很容易地进行此逻辑上的单元测试。 (我不介意使用测试框架,用户界面,但我想我测试尽可能在我的单元测试中完成的)。 一种解决方案是要设置格式显示,价格在产品类添加属性,但不是喜欢的执行,因为它意味着我的视图的问题开始泄漏到域层。 一些价格格式可能不看起来像一个大大量但问题似乎总是随着您的应用程序的增长大小,和引起问题的小项目开始增加一样简单。 渚嬪的方式 如果会发生什么情况需要在两个页面上以不同的方式显示价格? 将开始向您的域对象的每个不同版本的重载呢?
一个好方法是使用该 演示文稿模型模式. 演示文稿模型可以映射和从域对象,并可以保存特定于视图的逻辑的和值可以很容易地进行测试。 在示例应用程序中已为每个表单创建一个演示文稿的模型。 由于大多数窗体非常类似,它们将从共享的基类继承。 在更复杂的应用程序但是,它们将可能包含非常不同的代码,具体取决于窗体上的逻辑。
如果一个演示文稿的模型已采用上, 一个语句而如下:
<%= Html.Encode(this.Model.DisplayPrice) %>
这样 图 7 所示单位测试中,您可以轻松地测试此属性。 请注意在 DisplayPrice 上放置在抽象基类因此一个存根 (stub) 创建从以便测试该基类继承的。 请记住将存根 (stub) 是只返回假数据用于测试的对象。 因为您无法实例化抽象类,使用该存根 (stub) 测试基本功能。
图 7 单元测试的方法
[Fact]
public void PriceIsFormattedAsCurrency()
{
// Arrange
var product = new Product {Price = 9.22m};
var presentationProduct = new
PresentationProductStub(product);
// Act
string cost = presentationProduct.DisplayPrice;
// Assert
Assert.Equal("$9.22", cost);
}
PresentationProductStub 类按降序排列 PresentationProductBase 类映射与产品类中。 这会导致列表操作 ProductController 类看起来类似于下面这样:
public ActionResult List()
{
var products = productService.FindAll();
return View(new ProductList().MapList(products));
}
该产品的存在通常从服务,但它们被发送到视图之前,它们传递给方法的产品类别列表转换为一个类列表的范围的范围的类。 为您的视图中使用模型具有几个明显的优势。 第一次,您已经看到您可添加特定于视图的行为可以很容易地单元测试的类。 用于格式化或转换数据的显示。 第二个,可以使用内置 ASP.NET MVC 该默认模型活页夹,您的演示文稿模型放在 POST 方法的参数列表,如下所示:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(ProductCreate productCreate)
{
productService.Save(productCreate.GetProduct());
return RedirectToAction("List");
}
如果您不了解 ASP.NET MVC 默认模型联编程序的工作原理,它只是按到参数列表中的类的属性的名称映射在 HTML 中的字段。 如果我将产品类放在参数列表在前面的代码中,用户可能恶意 POST 的名为"ID"的数据,并覆盖在模型上的键。 这显然不是一件好事。 用户可能同样在我的类上的任何域的名称相匹配,并覆盖值。 在视图上使用模型允许您时仍然能够控制哪些属性映射到您的域对象使用该默认模型联编程序 (如果您选择)。
另一种方法是定义您可以使用映射到要保存到数据库的实体通过操作的方法参数传入的实体的映射类。 这使您可以控制要保存到数据库之前将复制的属性。 另一个选项是创建基于反射的映射工具,您可以使用映射的域类似于在 AutoMapper 工具赵 Bogard 通过创建。
测试路由
现在,带有很多高级体系结构来提示您的测试我们将重点更精细的测试方法。 在实现 ASP.NET MVC 应用程序第一步是编写测试以验证应用程序内路由的行为。 一定要确保基本工艺路线"~ /"将转发到相应的控制器和操作,和其他控制器和操作转发正确以及。 非常具有这些测试在从开始处的位置以便以后,将更多的路由添加到您的应用程序,您保证已定义的路由不是中断非常重要。
首先,定义默认的测试路由 (请参见 图 8 )。 在此应用程序中,必须在"类别"控制器定义了默认控制器,并且此控制器的默认操作设置为"列表"。 我们的测试应检查基本的路由,并断言的路由数据中包含正确的值。
图 8 测试路由数据
[Fact]
public void DefaultUrlRoutesToCategoryControllerAndListAction()
{
// Arrange
var context = ContextHelper.GetMockHttpContext("~/");
var routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);
// Act
RouteData routeData = routes.GetRouteData(context.Object);
// Assert
RouteTestingHelper.AssertRouteData(routeData, "Category", "List", "");
}
此测试使用一个帮助器函数声明如下所示的默认路由的值:
public static void AssertRouteData(RouteData routeData,
string controller, string action, string id)
{
Assert.NotNull(routeData);
Assert.Equal(controller, routeData.Values["controller"]);
Assert.Equal(action, routeData.Values["action"]);
Assert.Equal(id, routeData.Values["id"]);
}
使用此相同的常规测试方案,您可以在 HttpContext 传递控制器/操作/ID 模式 (如"~/Product/Edit/12") 的任何路由并断言值 (请参见 图 9 ).
图 9 测试帮助路由数据
[Fact]
public void ProductEditUrlRoutesToProductControllerAndListAction()
{
// Arrange
var context = ContextHelper.GetMockHttpContext("~/Product/Edit/12");
var routes = new RouteCollection();
MvcApplication.RegisterRoutes(routes);
// Act
RouteData routeData = routes.GetRouteData(context.Object);
// Assert
RouteTestingHelper.AssertRouteData(routeData, "Product", "Edit", "12");
}
使用此方法,测试的路由 (为清楚起见的学习),有点详细,它们肯定可以被缩短。 本 Scheirman 已经在此区域,一些有趣的工作,他很好的张贴内容在他的博客有关进行非常简洁的工艺路线断言 (" 在 ASP.NET MVC 中测试 fluent 路由").
将操作的依赖项作为参数传递
第二个提示是传递给控制器操作的所有依赖项,作为操作方法的参数。 我在示例应用程序文件下载的示例说明这一点。 正如我前面提到的那样,一个 HttpRequest ASP.NET 运行库中的将是一个静态的类,非常难以替换或单元测试中模拟。 在 ASP.NET MVC,允许被模拟,这些类提供了包装类,但模拟它们,或它们 stubbing 出的过程可能仍会很复杂。
若要模拟 HttpRequestBase 对象,您需要模拟 HttpContextBase 对象上的并为每个然后填充多个属性。 在保存文件的问题需要模拟 HttpContextBase 位于该 HttpServerUtilityBase。 而不是试图创建模仿该 HttpContextBase 的所有不同部分的多个模拟对象,它是很好,请在 HttpServerUtilityBase 操作方法的参数,然后将测试过程中传递中的单个 mocked 类:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, ViewProduct viewProduct,
HttpServerUtilityBase server, HttpPostedFileBase imageFile)
{
请注意,在前面的代码第三个参数是类型,我们需要使用,和我们需要模拟的类型。 使用如下方法的签名,我们只需要模拟 HttpServerUtilityBase 类。 如果我们有此类通过 this.HttpContext.Server,它了会需要模拟 HttpContextBase 类。 问题是只需添加 HttpServerUtilityBase 类为方法参数不会使其工作 ; 您必须提供一种告诉 ASP.NET MVC 如何实例化此类的方法。 这是框架的模型活页夹有位置。 请注意 HttpPostedFileBase 已经有一个自定义的模型联编程序默认情况下分配给它。
模型活页夹是实现 IModelBinder 接口,并提供一种 ASP.NET MVC 实例化操作方法的类型的方法的类。 在这种情况下我们需要在模型活页夹的该 HttpServerUtilityBase。 我们可以创建一个类,如下所示:
public class HttpServerModelBinder: IModelBinder
{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
return controllerContext.HttpContext.Server;
}
}
模型联编程序具有访问权限在控制器的上下文和一个绑定上下文。 此模型联编程序只需访问控制器的上下文并返回 HttpContext 类上的 HttpServerUtilityBase 类。 保留的是告诉 MVC ASP.NET 运行库使用此模型活页夹时发现类型 HttpServerUtilityBase 的参数。 请注意此代码放在 Global.asax 文件中,因此:
public void Application_Start()
{
// assign model binder for passing in the
// HttpServerUtilityBase to controller actions
ModelBinders.Binders.Add(typeof(HttpServerUtilityBase), new HttpServerModelBinder());
}
现在,当操作方法作为参数传递时的 HttpServerUtilityBase,MVC ASP.NET 运行库将调用上获取的类的实例 HttpServerModelBinder BindModel 方法。 创建模型活页夹的功能非常简单,它们使测试控制器操作变得更加容易。
测试操作的结果
在早期 ASP.NET MVC 抱怨之一有关测试的操作,如哪些视图已呈现的操作方法的困难。 您必须模拟控制器上下文、 在视图引擎,等等。 是真正的痛苦。 在预览 3,ASP.NET MVC 团队添加是类,降序从 ActionResult 类并表示该操作将执行的任务的操作结果的概念。 现在,在 ASP.NET MVC 中,每个操作的方法返回类型 ActionResult,并开发人员可以从许多内置的操作的结果类型中选择或创建他或她自己。 这有助于测试,因为您可以调用的操作方法,并检查结果,以查看内容发生。 让我们看一个我们可以创建控制器基类上,查看方法的调用,如下所示的 ViewResult 的示例:
public ActionResult List()
{
IEnumerable<Category> categories = categoryService.FindAll();
return View(ViewCategory.MapList(categories));
}
在此操作方法的类别列表模型作为传递给该视图,,但未视图提供名称。 这意味着此操作将会呈现在名为列表的默认视图。 如果您要确保此操作始终返回一个空的视图名称,您可以编写一个测试,如 图 10 。
图 10 测试操作结果
[Fact]
public void ListActionReturnsViewResultWithDefaultName()
{
// Arrange
var mockService = new Mock<ICategoryService>();
var controller = new CategoryController(mockService.Object);
// Act
var result = controller.List();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
Assert.Empty(viewResult.ViewName);
}
此测试 mocks 创建控制器类 CategoryService。然后测试调用以获取 ActionResult 对象列表操作。最后,测试断言结果的类型 ViewResult,并且该视图的名称为空。这会很容易地测试对某些已知的字符串值或的返回类型是一些更复杂 (JSON 格式,例如),检查 Data 属性以确保它包含预期的信息,通过该操作的方法调用返回结果的情况下视图名称。
推荐阅读
- Agile Principles, Patterns, and Practices in C# 通过 Robert C。马丁和 Micah 马丁 (Prentice 大厅,2006年)
- 企业应用程序体系结构的模式通过马丁 Fowler (Addison-Wesley 专业,2002年)
- Microsoft.NET: 构建企业应用程序Dino Esposito 和 Andrea Saltarello (微软出版社,2008年)
上的换行
ASP.NET MVC 团队已投入了大量的工作中创建一个灵活的体系结构允许简单的测试。开发人员现在可以更轻松地测试他们的系统的功能,如可插入的控制器工厂、 操作结果类型和 ASP.NET 上下文类型包装由于。开发所有 ASP.NET MVC 人员都提供一个好的起始点,但在 onus 仍在开发人员设计和构建的松散耦合和可测试的应用程序。我希望本文有助于为进度,向该路径下。如果您想我列出了这些建议阅读侧栏中。
索 Etheredge是一个 Microsoft C# MVP,在作者CodeThinked.comfounder Richmond 软件劳作组的和。在高级顾问,他是dominion 数字在弗吉尼亚 Richmond,州,他提供指导设计和构建系统的所有形状和大小的位置。