MVC4 / Mocking Controller.Request

时间:2015-04-09 14:57:37

标签: c# asp.net-mvc unit-testing mocking moq

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

在第一种情况下,必须传递包含在URL中的会话哈希。那个被禁止的,我决定将它作为Request标头传递给服务器。

示例:

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

    ClipGenerationResponse responseModel = new ClipGenerationResponse();            

    return Json(responseModel, JsonRequestBehavior.AllowGet);
}

现在的问题似乎是我无法将Request对象模拟为自定义对象,因为Request对象是一个只读对象。动态设置标头不起作用,因为在执行单元测试时Request为null。所以以下内容不起作用:

[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());
}

使用Moq模拟创建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";

所以,我想知道如何解决这个问题很好。我总是可以在构造函数中获取Request,设置一个类变量来保存Request,然后模拟getter / setter以进行测试,但我宁愿使用更好的方法来实现它。虽然我似乎不知道如何让它发挥作用。

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

更新

由于你似乎无法模拟HttpContext.Current.Request,我决定模拟HttpContext.Current。导致:

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

可悲的是,这适用于API,但不适用于单元测试,因为HttpContext不能被模拟,因为它不是一个接口。

  

初始化方法   SomeApiTest.Controllers.LoginControllerTest.Initialize   扔了例外。 System.NotSupportedException:   System.NotSupportedException:类型为mock必须是接口或   抽象或非密封的课程。

建议的方法是:

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

但这不起作用,因为请求无法强制转换为HttpRequestBase。

这意味着,我现在有办法对我的代码进行单元测试,但它将无法再运行..


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


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

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

但不适合运行时。因为: - 不发送其他标头,例如:UserHostAddress,Custom Headers

使用Postman设置每个请求一个自定义标头,名为“hash”。使用此方法,看起来不再设置这些标头。


看起来在调用方法时设置了标头,但在创建Controller本身时却没有。因此,对此的依赖注入可能不合适。


丑陋的临时修复:

    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());
    }

2 个答案:

答案 0 :(得分:1)

当你在Controller中引用静态类或控制器属性的东西时,你通常会自己开机,因为你的单元测试不能测试你的类,就像你的情况一样。为避免这种情况,您的Controller会使用IoC注入HttpRequestBase。例如,在注册模块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))
    );

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

这是我在项目中经常使用的方法。但是,即使不重写原始代码,也可以选择模拟Controller Request属性。它涉及使用Controller ControllerContext属性:

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();

答案 1 :(得分:0)

我的模拟请求看起来:

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并行,它完美无缺。