MVC4/模拟 Controller.Request

MVC4 / Mocking Controller.Request

我目前正在从事 MVC4 项目。当我做了一些重构时,测试也应该改变。

在第一种情况下,必须在 URL 中传递一个 session 散列。说到这里,我决定将它作为请求 header.

传递给服务器

示例:

[HttpPost]
public ActionResult Generate(ClipGenerateRequest request)
{
    string hash = Request.Headers["hash"];

    ClipGenerationResponse responseModel = new ClipGenerationResponse();            

    return Json(responseModel, JsonRequestBehavior.AllowGet);
}

现在的问题似乎是我无法将请求 object 模拟为自定义请求,因为请求 object 是 read-only object。动态设置 header 不起作用,因为在执行单元测试时请求为空。所以以下将不起作用:

[TestMethod]
public void GenerateTest()
{
    GenerationController target = new GenerationController(this.loginModelMock, this.clipTemplateModelMock, this.clipTemplateFieldModelMock);
    target.Request = this.requestBase;
    string templateId = "0";

    ActionResult actual = target.Generate(templateId);
    Assert.AreEqual(typeof(JsonResult), actual.GetType());
    Assert.AreEqual(typeof(ClipGenerationResponse), ((JsonResult)actual).Data.GetType());
}

其中 this.requestBase 将使用最小起订量模拟创建。

public HttpContextBase CreateMockHttpContext()
{
    var serverVariables = new NameValueCollection {
        { "UserHostAddress", "127.0.0.1" },
        { "UserAgent", "Unit Test Value" }
    };

    var httpRequest = new Moq.Mock<HttpRequestBase>();
    httpRequest.SetupGet(x => x.Headers).Returns(
        new System.Net.WebHeaderCollection {
            {"hash", "somehash"}
        }
    );

    httpRequest.Setup(x => x.ServerVariables.Get(It.IsAny<string>()))
        .Returns<string>(x =>
            {
                return serverVariables[x];
            });

    var httpContext = (new Moq.Mock<HttpContextBase>());
    httpContext.Setup(x => x.Request).Returns(httpRequest.Object);

    return httpContext.Object;
}

静态方式也行不通:

target.Request.Headers["hash"] = "hash";

所以,我想知道如何很好地 解决这个问题。我总是可以在构造函数中获取请求,设置一个 class 变量来保存请求,然后模拟 getter / setter 用于测试目的,但我宁愿使用更好的方法来做到这一点。虽然我似乎不知道如何让它工作。

PS:请注意,某些 class 名称可能已更改以供预览。

更新

由于您似乎无法模拟 HttpContext.Current.Request,我决定模拟 HttpContext.Current。结果:

container.RegisterInstance<HttpRequest>(HttpContext.Current.Request);

遗憾的是,这适用于 API,但不适用于单元测试,因为 HttpContext 不能模拟,因为它不是接口。

Initialization method SomeApiTest.Controllers.LoginControllerTest.Initialize threw exception. System.NotSupportedException: System.NotSupportedException: Type to mock must be an interface or an abstract or non-sealed class. .

建议的方法是:

container.RegisterInstance<HttpRequestBase>(HttpContext.Current.Request);

但这不起作用,因为 Request 无法转换为 HttpRequestBase。

这意味着,我现在可以对我的代码进行单元测试,但它将不再能够 运行..


正在测试是否可以使用 HttpRequestWrapper 解决此问题。


看起来以下内容确实适用于测试:

HttpRequestBase requestBase = new HttpRequestWrapper(HttpContext.Current.Request);
container.RegisterInstance<HttpRequestBase>(requestBase);

但不是 运行时间。因为: - 不发送其他 header,例如:UserHostAddress、Custom Headers

对于 Postman,每个请求都会设置一个名为 "hash" 的自定义 header。使用此方法,这些 header 似乎不再设置。


看起来 headers 是在调用方法时设置的,而不是在创建控制器本身时设置的。因此依赖注入可能不适合。


丑陋的临时修复:

    private AuthenticationHelper authenticationHelper = null;
    private ILoginModel iLoginModel = null;
    private IModuleModel iModuleModel = null;
    private HttpRequestBase iRequestBase = null;

    public LoginController(ILoginModel loginModel, IModuleModel moduleModel, HttpRequestBase requestBase)
    {
        this.authenticationHelper = new AuthenticationHelper(loginModel);
        this.iLoginModel = loginModel;
        this.iModuleModel = moduleModel;
        this.iRequestBase = requestBase;
    }

    private HttpRequestBase GetRequestBase()
    {
        if (Request != null)
        {
            return Request;
        }
        else
        {
            return iRequestBase;
        }
    }

    [HttpPost]
    public ActionResult Login(LoginRequest login)
    {
        var ip = this.authenticationHelper.GetIpAddress(GetRequestBase());
        var userAgent = this.authenticationHelper.GetUserAgent(GetRequestBase());
    }

在 Controller 中,当你通过静态 classes 或 Controller 属性引用某些内容时,你通常会在启动时自行射击,因为你使你的 class 无法通过单元测试进行测试,就像你的情况一样.为避免这种情况,您的控制器将 HttpRequestBase 注入 IoC。例如,在注册模块 AutofacWebTypesModule 后使用 Autofac,您的控制器可能会在其 HttpRequestBase 类型的构造函数参数中接受基本上是您通过 Register 属性 获得的内容。所以你应该让你的控制器看起来像这样:

public class GenerationController : Controller
{
    readonly HttpRequestBase _request;
    public GenerationController(
            HttpRequestBase request,
            // Additional constructor parameters goes here
        )
    {
        _request = request;
    }
}

使用 Unity,您应该像这样为 HttpRequestBase 注册工厂:

container
    .RegisterType<HttpRequestBase>(
        new InjectionFactory(c => new HttpRequestWrapper(HttpContext.Current.Request))
    );

当您在单元测试中创建控制器时,模拟 HttpRequestBase 非常容易,因为它的所有属性以及 Headers 都是虚拟的。

这是我在项目中经常使用的方法。但是,即使不重写原始代码,也可以选择模拟控制器请求 属性。涉及到ControllerControllerContext的使用属性:

Mock<HttpContextBase> httpContextMock = new Mock<HttpContextBase>();
Mock<HttpRequestBase> httpReguestMock = new Mock<HttpRequestBase>();
httpContextMock.SetupGet(c => c.Request).Returns(httpReguestMock.Object);
GenerationController controller = new GenerationController();

controller.ControllerContext = new ControllerContext(httpContextMock.Object, new RouteData(), controller);
controller.Index();

我的模拟请求看起来:

var request = A.Fake<HttpRequestBase>();
var formCollection = new NameValueCollection();
A.CallTo(() => request.HttpMethod).Returns("POST");
A.CallTo(() => request.Headers).Returns(new System.Net.WebHeaderCollection
{
        {"X-Requested-With", "XMLHttpRequest"}
});
A.CallTo(() => request.Form).Returns(formCollection);
A.CallTo(() => request.ApplicationPath).Returns("/");

根据我的需要,它可以工作。我正在使用 FakeItEasy 来模拟请求,但我很确定您可以改用 Moq。我在我的项目中并行使用了 FakeItEasy 和 Moq,而且效果很好。