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,而且效果很好。
我目前正在从事 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,而且效果很好。