# 十、MVC 与 FakeItEasy

## 简介

HttpContext。

只是那班的声音让最有经验的。网络开发人员战战兢兢。HttpContext 是人类历史上创造的最大的密封对象，一个真正的微软宝藏。一个宝藏，不幸的是，我们都被卡住了。

HttpContext 的真正问题不是它巨大且密封的事实(尽管这没有帮助)，而是在 MVC 控制器中接触和使用 HttpContext 是如此容易，甚至不知道你已经做到了。

话虽如此，我有个任务给你。

就是看[这篇博文](http://volaresystems.com/blog/post/2010/08/19/Dont-mock-HttpContext)关于如何模拟 HttpContext。这是充分利用这一部分的必要阅读，所以请现在就停下来，完整地阅读这篇文章，完成后再回到书中。

在阅读这篇博文之前，我没有意识到试图嘲笑 HttpContext 甚至打败了强大的查克·诺里斯……但是让我们来谈谈作者概述的解决方案。

## 万物的接口！(没那么快……)

本文展示了作者是如何通过一个接口抽象出 HttpContext 片段的，该接口允许他以前不可测试的控制器类是可测试的。

例如，作者创建了`ICurrentUser`接口，该接口包装了 HttpContext 调用，然后将该接口注入到`OrderController`中。

也许你在想你将要编写的模拟接口的 FakeItEasy 代码，以允许`OrderController`是可测试的。这是一个可行的解决方案，并且会起作用，但是让我们看看一些缺点。

HttpContext 是巨大的。如果你需要伪造的不仅仅是对 HttpContext 的几次调用。`Current.User.IsInRole`(如 HttpRequest、HttpResponse、HttpSessionState 等。)，你最终要么编写一个包含一堆成员的巨大接口，这违反了[接口隔离原则](http://www.objectmentor.com/resources/articles/isp.pdf)，要么编写一堆小接口，开始用依赖关系淹没控制器的构造器。您仍然需要在控制器的构造函数中为其他依赖项留出空间，比如存储库。

一定有更好的方法。

有，使用 FakeItEasy 的 MVC 的控制器扩展点，和新的“基础”类。NET 已经添加到它的框架中，允许测试那些密封的类。事情是这样的。

## MVC 和 ControllerContext

如果我们看看所有 MVC 控制器继承的`ControllerBase`类，我们会看到一个名为`ControllerContext` `.`的属性

```cs
    public abstract class ControllerBase : IController
    {
        protected ControllerBase();
        public ControllerContext ControllerContext { get; set; }
        public TempDataDictionary TempData { get; set; }
        public bool ValidateRequest { get; set; }
        public IValueProvider ValueProvider { get; set; }
        [Dynamic]
        public dynamic ViewBag { get; }
        public ViewDataDictionary ViewData { get; set; }
        protected virtual void Execute(System.Web.Routing.RequestContext requestContext);
        protected abstract void ExecuteCore();
        protected virtual void Initialize(System.Web.Routing.RequestContext
           requestContext);
    }

```

代码清单 132:controller base 类的 ControllerContext 属性

正是这个由微软添加到控制器继承结构中的可扩展性点，将允许我们使用 FakeItEasy 来控制像 HttpResponse、HttpRequest、HttpSessionState 以及其他 FakeItEasy 无法控制的密封类。

我们来看看`ControllerContext`班。

```cs
    public class ControllerContext
    {
        public ControllerContext();
        protected ControllerContext(ControllerContext controllerContext);
        public ControllerContext(RequestContext requestContext, ControllerBase controller);
        public ControllerContext(HttpContextBase httpContext, RouteData routeData,
            ControllerBase controller);
        public virtual ControllerBase Controller { get; set; }
        public IDisplayMode DisplayMode { get; set; }
        public virtual HttpContextBase HttpContext { get; set; }
        public virtual bool IsChildAction { get; }
        public ViewContext ParentActionViewContext { get; }
        public RequestContext RequestContext { get; set; }
        public virtual RouteData RouteData { get; set; }
    }

```

代码清单 133:controller context 类

构造函数有四个重载，包括一个不带参数的重载。我们将探索需要三个参数的过载:
`public ControllerContext(HttpContextBase httpContext` `,` `RouteData routeData` `,` `and` `ControllerBase controller)` `.`

但是首先，什么是`HttpContextBase`？`HttpContext`怎么了？

## 将“基”放入系统。网络的密封类

回到本章开头的“查克·诺里斯”例子，微软知道它需要做一些事情来让 MVC 成为一个单元测试友好的框架。它在系统中的当前类别列表。Web 库由所有密封的类组成，这些类不能被大多数嘲笑和伪造的框架测试。

因此，他们创建了通过 MVC 框架公开的“基础”类。在运行时，这些“基础”类被委托给真正的静态类。

例如，这里是`HttpContext`(不可测试)的类声明:

```cs
    public sealed class HttpContext : IServiceProvider, IPrincipalContainer

```

代码清单 134:HttpContext 的类声明；不可测试，因为它是密封的

这里是`HttpContextBase`(可测试)的类声明:

```cs
    public abstract class HttpContextBase : IServiceProvider

```

代码清单 135:HttpContextBase 的类声明

回到第 3 章“介绍 FakeItEasy”，我们记得你不能用 FakeItEasy 伪造一个密封类。

那么是什么让`HttpContextBase`的成员可以造假呢？每一个都被声明为虚拟的。通过将成员声明为虚拟的，您允许 FakeItEasy 能够在配置、行为和断言中使用它们。这同样适用于他们在运行时委托给的`HttpRequestBase`和`HttpResponseBase`类及其各自的不可测试类(`HttpRequest`和`HttpResponse`)。

## 用 FakeItEasy 设置 ControllerContext

现在我们已经对系统中的“基础”类有了坚实的理解。Web，让我们看看如何使用 FakeItEasy 来设置我们在创建`ControllerContext`时需要的赝品。

首先，让我们构建一个非常简单的 MVC 控制器，并将其称为`HomeController`:

```cs
    public class HomeController : Controller
    {
        [HttpPost]
        public void WriteToResponse()
        {
            Response.Write("writing to response");
        }    
    }

```

代码清单 136:家庭控制器类

`HomeController`上的`WriteToResponse`方法向响应流中写入一个字符串。通常，考虑到 MVC 模型的丰富性和模型绑定支持，我们很少执行这样的操作，但是为了示例，让我们坚持使用这个动作方法作为起点。

下面是测试代码清单 136 中的 MVC 动作方法的 FakeItEasy 设置方法:

```cs
    [TestFixture]
    public class WhenWritingToResponse
    {
        private HttpResponseBase httpResponse;

        [SetUp]
        public void Given()
        {
            var sut = new HomeController();
            var httpContext = A.Fake<HttpContextBase>();
            httpResponse = A.Fake<HttpResponseBase>();
            A.CallTo(() => httpContext.Response).Returns(httpResponse);
            var context = new ControllerContext(new RequestContext(httpContext,
                new RouteData()), sut);

            sut.ControllerContext = context;
            sut.WriteToResponse();
        }
    }

```

代码清单 137:测试家庭控制器的类的设置方法

更新我们的 SUT 就像往常一样——直接更新它，然后将任何依赖传递给它的构造函数。由于`HomeController`没有依赖关系，在这个例子中创建 SUT 非常简单。

在下一行，你可以看到我们正在创建一个`HttpContextBase`类的赝品:
`var httpContext = A.Fake<HttpContextBase>();`

在我们创建了`HttpContextBase`的赝品之后，我们创建了`HttpResponseBase` :
`httpResponse = A.Fake<HttpResponseBase>();`的赝品

一旦这两个赝品都被创建，我们现在可以配置调用赝品`HttpContextBase`的`Response`属性将返回赝品`HttpResponseBase`。

现在我们终于可以开始创建我们的`ControllerContext`类了。我们这样做是通过传入一个新的`RequestContext`，并将伪造的`HttpContextBase`、一个新的`RouteData`以及该上下文将用于的控制器传入`RequestContext`的构造器。

为了完成测试设置，我们将 SUT 的`ControllerContext`属性分配给新创建的`ControllerContext`对象，我们将配置的赝品传递给该对象。完成后，我们在 SUT 上调用`WriteToResponse`动作方法

## 单元测试

在这种情况下，我们需要测试的非常非常简单。我们想断言一个“写响应”的消息恰好被传递给了`Response`对象的`Write`方法一次。代码如下所示:

```cs
    [Test]
    public void WritesToResponse()
    {
        A.CallTo(() => httpResponse.Write("writing to response"))
            .MustHaveHappened(Repeated.Exactly.Once);
    }

```

代码清单 138:测试响应是否编写了正确的字符串。写

与我们的设置方法相比，这段代码看起来非常熟悉。乍一看，你甚至不知道我们在测试一个 MVC 控制器；代码读起来和我们到目前为止在书中看到的大多数其他测试方法一样。

但是测试比写响应流更复杂的东西呢？让我们扩展我们的例子来执行一些更常见的 MVC 任务。

## 假装会话

很多时候，我们在控制器中使用`Session`。无论是阅读还是写作，`Session`是`HttpContext`中另一个很容易接触和使用的项目。幸运的是，微软提供了`HttpSessionStateBase`类，其中所有成员都被声明为虚拟的。

这里有一个新的动作方法，我们在`Session`中添加一个新的项目:

```cs
    [HttpPost]
    public void AddCustomerEmailToSession(string customersEmail)
    {
        Session.Add("CustomerEmail", customersEmail);
    }

```

代码清单 139:新的 AddCustomerEmailToSession 操作方法

这是我们新的测试设置，将会话状态包含在我们伪造的`HttpContextBase`中:

```cs
    [TestFixture]
    public class WhenAddingToSession
    {
        private const string customerEmail = "customer@email.com";
        private HttpSessionStateBase httpSession;

        [SetUp]
        public void Given()
        {
            var sut = new HomeController();

            var httpContext = A.Fake<HttpContextBase>();
            var httpResponse = A.Fake<HttpResponseBase>();
            httpSession = A.Fake<HttpSessionStateBase>();

            A.CallTo(() => httpContext.Response).Returns(httpResponse);
            A.CallTo(() => httpContext.Session).Returns(httpSession);

            var context = new ControllerContext(new RequestContext(httpContext,
                new RouteData()), sut);
            sut.ControllerContext = context;

            sut.AddCustomerEmailToSession(customerEmail);
        }
    }

```

代码清单 140:测试设置包括一个伪造的 HttpSessionStateBase

我们正在创建一个`HttpSessionStateBase`的赝品，然后将该赝品设置为在调用赝品`HttpContextBase`上的`Session`属性时返回。通过建立这个赝品“链条”，我们现在可以完全控制控制器中`HttpContext`发出的最常见的调用。

下面的测试断言我们将客户的电子邮件地址添加到了`Session`:

```cs
    [Test]
    public void AddCustomerEmailToSession()
    {
        A.CallTo(() => httpSession.Add("CustomerEmail", customerEmail))
            .MustHaveHappened(Repeated.Exactly.Once);
    }

```

代码清单 141:向会话添加客户电子邮件的单元测试

## 伪造原则

到目前为止，我们已经了解了如何伪造`HttpContextBase`、`HttpRequestBase`、`HttpResponseBase`和`HttpSessionStateBase`。这涵盖了您将在 MVC 控制器中使用的大量代码类型，但是 MVC 应用中有一个很大的部分我们还没有谈到，那就是身份验证和授权。让我们看看如何在控制器的动作方法中单元测试使用`IPrincipal`的代码。

| ![](img/note.png) | 注意:你可以在这里找到更多关于 IPrincipal [的信息。](https://msdn.microsoft.com/en-us/library/system.security.principal.iprincipal(v=vs.110).aspx) |

假设在我们发送电子邮件之前，我们想要访问控制器类上的`User`属性，并获取经过身份验证的用户的当前名称。我们通过访问控制器动作方法中的`User.Identity.Name`来实现这一点。下面是一个使用我们当前的`HomeController`的例子。

我在这个例子中添加了`ISendEmail`接口，并将其注入到`HomeController`的构造函数中。

```cs
    public class HomeController : Controller
    {
        private readonly ISendEmail sendEmail;

        public HomeController(ISendEmail sendEmail)
        {
            this.sendEmail = sendEmail;
        }

        [HttpPost]
        public void SendCustomerEmail(string to)
        {
            var user = User.Identity.Name;
            sendEmail.SendEmailTo("somecompany@somewhere.com", to,
                string.Format("This email is intended for {0}", user), "this is an email");
        }
    }

```

代码清单 142:使用用户。标识。用于获取经过身份验证的用户的名称的名称

您可以看到我们在哪里联系`User.Identity.Name`获取当前认证用户的名称。默认情况下，`User`属性作为所有控制器继承的`public abstract class Controller`类的只读属性公开。

基本控制器类的属性类型为`IPrinciple`:

```cs
    public IPrincipal User { get; }

```

代码清单 143:抽象控制器类的用户属性

看`IPrincipal`，我们看到如下:

```cs
    public interface IPrincipal
    {
        IIdentity Identity { get; }
        bool IsInRole(string role);
    }

```

代码清单 144:IPrincipal 接口

从这个界面定义可以看到`Identity`属性，是`IIdentity`的一种类型。

```cs
    public interface IIdentity
    {
        string AuthenticationType { get; }
        bool IsAuthenticated { get; }
        string Name { get; }
    }

```

代码清单 145:身份接口

在运行时，。NET 提供了一个`IPrincipal`的实现，但是在单元测试的时候，并没有。我们来看看如何用 FakeItEasy 来伪造`IPrincipal`让`SendCustomerEmail`动作法可测试。

`Here is our test setup:`

```cs
    [TestFixture]
    public class WhenSendingCustomerEmail
    {
        private ISendEmail emailSender;
        private const string emailAddress = "customer1@somewhere.com";
        private const string userName = "UserName";

        [SetUp]
        public void Given()
        {
            emailSender = A.Fake<ISendEmail>();
            var sut = new HomeController(emailSender);
            sut.ControllerContext = new ControllerContext(new RequestContext(
                A.Fake<HttpContextBase>(), new RouteData()), sut);

            var principal = A.Fake<IPrincipal>();
            var identity = new GenericIdentity(userName);
            A.CallTo(() => principal.Identity).Returns(identity);
            A.CallTo(() => sut.ControllerContext.HttpContext.User).Returns(principal);   

            sut.SendCustomerEmail(emailAddress);
        }
    }

```

代码清单 146:使用用户的控制器动作方法的单元测试设置。身份，名字

既然我们已经添加了控制器的动作方法要使用的`ISendEmail`接口，我们首先创建一个`ISendEmail`的赝品，并将这个赝品传递给 HomeController 的构造函数。我们将在我们的测试方法中使用这个配置好的伪断言。

接下来，我们简化了`ControllerContext`的设置，主要是因为我们真正需要的只是一个伪造的`HttpContextBase`。我们不需要设置一个假的`HttpRequestBase`、`HttpResponseBase`或`HttpSessionStateBase`，因为我们在测试中没有使用任何代码。

在我们的 SUT 上设置`ControllerContext`之后，我们创建一个`IPrincipal`的赝品，创建一个`GenericIdentity`，然后当我们伪造的`IPrincipal`上的`Identity`属性被调用时，返回该身份。最后一行代码配置我们的`ControllerContext`上的`User`属性。`HttpContext`财产归还我们伪造的`IPrincipal`。

最后，现在我们已经创建并配置了我们的赝品，我们称之为 SUT 的`SendCustomerEmail`方法，传递给它一个电子邮件地址。

下面是单元测试方法:

```cs
    [Test]
    public void SendsEmailToCustomerWithUserNameInSubject()
    {
        A.CallTo(() => emailSender.SendEmailTo("somecompany@somewhere.com", emailAddress,
            string.Format("This email is intended for {0}", userName), "this is an email"))
                .MustHaveHappened(Repeated.Exactly.Once);
    }

```

代码清单 147:发送客户的单元测试

我们使用我们伪造的`EmailSender`来断言`SendEmailTo`是用正确的参数调用的，其中一个参数包括我们通过我们伪造的`IPrincipal`配置的认证用户的用户名。

## 伪造 UrlHelper

首先，什么是`UrlHelper`？如果您在控制器动作方法中必须构造一个 URL，并且您已经编写了如下代码，那么您之前已经使用过`UrlHelper`。

```cs
    var returnUrl = Url.Action("Index", "Home", null, Request.Url.Scheme);

```

代码清单 148:在控制器中使用 UrlHelper

如果您将光标放在`Url.Action`的“Url”部分上单击 F12，您会发现自己处于抽象控制器基类的`Url`属性中:

```cs
    public UrlHelper Url { get; set; }

```

代码清单 149:URl 属性是 UrlHelper 的类型

既然知道了`UrlHelper`是什么，我们就来看看如何用 FakeItEasy 测试一下。

让我们回到我们的`HomeController`类，添加另一个名为`BuildUrl`的动作方法。

```cs
    public ActionResult BuildUrl()
    {
        var model = new BuildUrl { Url = Url.Action("Index", "Home", null,
            Request.Url.Scheme) };
        return View(model);
    }

```

代码清单 150:构建 Url 操作方法

`BuildUrl`方法创建并填充一个`BuildUrl`模型类，然后在返回的视图中返回填充的模型。这里是`BuildUrl`模特班。

```cs
    public class BuildUrl
    {
        public string Url { get; set; }
    }

```

代码清单 151:构建 Url 模型类

注意`BuildUrl`法中`Url.Action`和`Request.Url.Scheme`的用法。我们需要这两个项目的赝品，以便使这个方法可测试。让我们从这个动作方法的单元测试的设置方法开始。

```cs
    [TestFixture]
    public class WhenBuildingUrl
    {     private HomeController sut;
        private string fullyQualifiedUrl;
        const string uri = "http://www.somewhere.com";

        [SetUp]
        public void Given()
        {
            sut = new HomeController();

            var httpContext = A.Fake<HttpContextBase>();
            var httpRequest = A.Fake<HttpRequestBase>();
            var httpResponse = A.Fake<HttpResponseBase>();

            A.CallTo(() => httpContext.Request).Returns(httpRequest);
            A.CallTo(() => httpContext.Response).Returns(httpResponse);

            var context = new ControllerContext(new RequestContext(httpContext,
                new RouteData()), sut);
            sut.ControllerContext = context;

            var fakeUri = new Uri(uri);
            A.CallTo(() => sut.ControllerContext.RequestContext.HttpContext.Request.Url)
                .Returns(fakeUri);

            fullyQualifiedUrl = string.Format("{0}/home/index", uri);
            sut.Url = A.Fake<UrlHelper>();
            A.CallTo(() => sut.Url.Action(A<string>.Ignored, A<string>.Ignored, null,
                A<string>.Ignored)).Returns(fullyQualifiedUrl);
        }
    }

```

代码清单 152:构建 Url 的测试设置

让我们从本章到目前为止看到的伪造`UrlHelper`的设置中探索一些差异:

*   我们在创建`HttpRequestBase`的赝品。`HttpRequestBase`具有在控制器`Request.Url.Scheme`中用于该代码的`Url`属性。为了确保在我们运行单元测试时`Request`不为空，我们创建了一个它的赝品。
*   一旦我们创建了一个假的 HttpRequestBase，我们就配置对`sut.ControllerContext.RequestContext.HttpContext.Request.Url`的调用，以返回我们将要构建的 URL。我们通过用以下代码行填充`fakeUri`变量来做到这一点:`var fakeUri = new Uri(uri);`
*   接下来，我们用字符串填充`fakeUri`变量。格式化调用。然后我们创造了一个假的`UrlHelper`。再次，对控制器中`Url.Action`的调用使用`UrlHelper`。一旦我们创建了我们的赝品，我们就使用我们的`uri`变量对其进行配置以返回正确的动作。

测试设置的其余部分与本章前面的示例非常相似。

让我们来看看我们的测试方法。

```cs
    [Test]
    public void ReturnsTheCorrectUrl()
    {
        var result = (ViewResult)sut.BuildUrl();
        var model = (BuildUrl)result.Model;
        Assert.That(model.Url, Is.EqualTo(fullyQualifiedUrl));
    }

```

代码清单 153:测试构建 Url 的断言

这里，我们断言模型的`Url`属性等于我们在单元测试设置方法中设置的`fullyQualifiedUrl`。

## 总结

在本章中，我们已经看到了如何将 FakeItEasy 与 MVC 最新的扩展点和结合使用。NET 的基类来创建完全可测试的控制器操作方法。我们介绍了如何围绕微软的大多数大型“密封”类进行设置和工作，以允许所有 MVC 操作方法的可测试性。当您在系统中编写控制器测试时，您将开始编写大量看起来相同的代码。请随意尝试将假的设置调用放入共享库中，或者像我们在当前工作中所做的那样，为您的单元测试项目中的 Controller 类编写一些扩展方法。