单元测试时如何 Mock/Stub 或干脆忽略 HttpRequest
How to Mock/Stub or simply ignore HttpRequest when unit testing
public class DemoController : Controller
{
private readonly ICommonOperationsRepository _commonRepo;
public DemoController (ICommonOperationsRepository commonRepo)
{
_commonRepo = commonRepo;
}
public ActionResult Default()
{
var model = new DemoModel();
try
{
**DeviceDetection dd = new DeviceDetection(Request.ServerVariables["HTTP_X_REWRITE_URL"].ToString());
dd.DetectDevice();**
model.ListTopListing.AddRange(_commonRepo.GetListings());
}
catch (Exception ex)
{
ExceptionHandler objErr = new ExceptionHandler(ex, "DemoController .Default()\n Exception : " + ex.Message);
objErr.LogException();
}
return View(model);
}
}
问题:DeviceDetection 在这里有具体的依赖关系,所以我无法对我的控制器进行单元测试。我不想模拟 Http 请求,因为我只想测试控制器而不是 DeviceDetection 模块。
我如何 mock/avoid 访问这个 (Request.ServerVariables["HTTP_X_REWRITE_URL"].ToString())
这是所有问题的根源。
使 DeviceDetection
成为 DemoController
的 concrete dependency:
public class DemoController : Controller
{
private readonly ICommonOperationsRepository _commonRepo;
private readonly DeviceDetection dd;
public DemoController (
ICommonOperationsRepository commonRepo,
DeviceDetection dd)
{
_commonRepo = commonRepo;
this.dd = dd;
}
public ActionResult Default()
{
var model = new DemoModel();
try
{
this.dd.DetectDevice();
model.ListTopListing.AddRange(_commonRepo.GetListings());
}
catch (Exception ex)
{
ExceptionHandler objErr = new ExceptionHandler(ex, "DemoController .Default()\n Exception : " + ex.Message);
objErr.LogException();
}
return View(model);
}
}
这应该使您能够创建 DemoController
的实例,而无需依赖 Request
属性:
var sut = new DemoController(someStupRepository, new DeviceDetection("foo"));
您可以在例如单元测试。
当您在应用程序中编写 DemoController
时,您将 request.ServerVariables["HTTP_X_REWRITE_URL"].ToString()
传递给 DeviceDetection
。您可以从 CreateController
的 requestContext
参数中获取 request
变量。
要回答您的问题,您需要在测试中满足以下条件:
var requestBase = new Mock<HttpRequestBase>();
requestBase.Setup(r => r.ServerVariables)
.Returns(new NameValueCollection { {"HTTP_X_REWRITE_URL", "your url"} });
var httpContext = new Mock<HttpContextBase>();
httpContext.Setup(x => x.Request).Returns(requestBase.Object);
var ctrCtx = new Mock<ControllerContext>();
ctrCtx.Setup(x => x.HttpContext).Returns(httpContext.Object);
demoController.ControllerContext = ctrCtx.Object;
然而,正如@Mark 建议的那样,您不需要在操作中创建 DeviceDetection
的具体实例,您需要注入它。但是与其注入一个具体的实例,不如将其包装到接口 IDeviceDetector
中并注入这个抽象。
我会给出一些优点:
- 您的操作不依赖于
DeviceDetection
的实施
Mock<IDeviceDetection>
允许您在设置时引发异常,以测试 try-catch
块的异常处理。
- 您可以断言
DetectDevice()
方法被调用
另一个建议 - 永远不要使用 try{} catch(Exception ex){}
,你应该只捕获那些你可以处理的异常。因为您不知道可以抛出哪种类型的异常以及如何有效地处理它,例如它可以是 OutOfMemoryException
。 This article 可以为您提供在 MVC 中处理异常的不同方式的基本思路。
更新: 如我所见,您正在使用 Unity 作为 IoC 容器。 Unity有可能inject constructor parameters。因此,您需要再次从 DeviceDetector
中提取接口,比方说 IDeviceDetector
。注册它
container.RegisterType<IDeviceDetector, DeviceDetector>(new InjectionConstructor(
HttpContext.Current.Request.ServerVariables["HTTP_X_REWRITE_URL"].ToString()));
用 TransientLifetimeManager
注册 DeviceDetector
。
那么你的控制器应该看起来像
public class DemoController : Controller
{
private readonly ICommonOperationsRepository _commonRepo;
private readonly IDeviceDetection _deviceDetection;
public DemoController (
ICommonOperationsRepository commonRepo,
IDeviceDetection deviceDetection)
{
_commonRepo = commonRepo;
_deviceDetection = deviceDetection;
}
public ActionResult Default()
{
var model = new DemoModel();
_deviceDetection.DetectDevice();
model.ListTopListing.AddRange(_commonRepo.GetListings());
return View(model);
}
}
请注意,在这种情况下,您需要为您的 Unity 容器编写单元测试以验证您的注入是否已正确解析。您的单元测试可能如下所示:
[TestMethod]
public void Test()
{
var repository = new Mock<ICommonOperationsRepository>();
var deviceDetection = new Mock<IDeviceDetection>();
var controller = new DemoController(repository.Object, deviceDetection.Object);
controller.Default();
deviceDetection.Verify(x => x.DetectDevice(), Times.Once());
}
public class DemoController : Controller
{
private readonly ICommonOperationsRepository _commonRepo;
public DemoController (ICommonOperationsRepository commonRepo)
{
_commonRepo = commonRepo;
}
public ActionResult Default()
{
var model = new DemoModel();
try
{
**DeviceDetection dd = new DeviceDetection(Request.ServerVariables["HTTP_X_REWRITE_URL"].ToString());
dd.DetectDevice();**
model.ListTopListing.AddRange(_commonRepo.GetListings());
}
catch (Exception ex)
{
ExceptionHandler objErr = new ExceptionHandler(ex, "DemoController .Default()\n Exception : " + ex.Message);
objErr.LogException();
}
return View(model);
}
}
问题:DeviceDetection 在这里有具体的依赖关系,所以我无法对我的控制器进行单元测试。我不想模拟 Http 请求,因为我只想测试控制器而不是 DeviceDetection 模块。
我如何 mock/avoid 访问这个 (Request.ServerVariables["HTTP_X_REWRITE_URL"].ToString())
这是所有问题的根源。
使 DeviceDetection
成为 DemoController
的 concrete dependency:
public class DemoController : Controller
{
private readonly ICommonOperationsRepository _commonRepo;
private readonly DeviceDetection dd;
public DemoController (
ICommonOperationsRepository commonRepo,
DeviceDetection dd)
{
_commonRepo = commonRepo;
this.dd = dd;
}
public ActionResult Default()
{
var model = new DemoModel();
try
{
this.dd.DetectDevice();
model.ListTopListing.AddRange(_commonRepo.GetListings());
}
catch (Exception ex)
{
ExceptionHandler objErr = new ExceptionHandler(ex, "DemoController .Default()\n Exception : " + ex.Message);
objErr.LogException();
}
return View(model);
}
}
这应该使您能够创建 DemoController
的实例,而无需依赖 Request
属性:
var sut = new DemoController(someStupRepository, new DeviceDetection("foo"));
您可以在例如单元测试。
当您在应用程序中编写 DemoController
时,您将 request.ServerVariables["HTTP_X_REWRITE_URL"].ToString()
传递给 DeviceDetection
。您可以从 CreateController
的 requestContext
参数中获取 request
变量。
要回答您的问题,您需要在测试中满足以下条件:
var requestBase = new Mock<HttpRequestBase>();
requestBase.Setup(r => r.ServerVariables)
.Returns(new NameValueCollection { {"HTTP_X_REWRITE_URL", "your url"} });
var httpContext = new Mock<HttpContextBase>();
httpContext.Setup(x => x.Request).Returns(requestBase.Object);
var ctrCtx = new Mock<ControllerContext>();
ctrCtx.Setup(x => x.HttpContext).Returns(httpContext.Object);
demoController.ControllerContext = ctrCtx.Object;
然而,正如@Mark 建议的那样,您不需要在操作中创建 DeviceDetection
的具体实例,您需要注入它。但是与其注入一个具体的实例,不如将其包装到接口 IDeviceDetector
中并注入这个抽象。
我会给出一些优点:
- 您的操作不依赖于
DeviceDetection
的实施
Mock<IDeviceDetection>
允许您在设置时引发异常,以测试try-catch
块的异常处理。- 您可以断言
DetectDevice()
方法被调用
另一个建议 - 永远不要使用 try{} catch(Exception ex){}
,你应该只捕获那些你可以处理的异常。因为您不知道可以抛出哪种类型的异常以及如何有效地处理它,例如它可以是 OutOfMemoryException
。 This article 可以为您提供在 MVC 中处理异常的不同方式的基本思路。
更新: 如我所见,您正在使用 Unity 作为 IoC 容器。 Unity有可能inject constructor parameters。因此,您需要再次从 DeviceDetector
中提取接口,比方说 IDeviceDetector
。注册它
container.RegisterType<IDeviceDetector, DeviceDetector>(new InjectionConstructor(
HttpContext.Current.Request.ServerVariables["HTTP_X_REWRITE_URL"].ToString()));
用 TransientLifetimeManager
注册 DeviceDetector
。
那么你的控制器应该看起来像
public class DemoController : Controller
{
private readonly ICommonOperationsRepository _commonRepo;
private readonly IDeviceDetection _deviceDetection;
public DemoController (
ICommonOperationsRepository commonRepo,
IDeviceDetection deviceDetection)
{
_commonRepo = commonRepo;
_deviceDetection = deviceDetection;
}
public ActionResult Default()
{
var model = new DemoModel();
_deviceDetection.DetectDevice();
model.ListTopListing.AddRange(_commonRepo.GetListings());
return View(model);
}
}
请注意,在这种情况下,您需要为您的 Unity 容器编写单元测试以验证您的注入是否已正确解析。您的单元测试可能如下所示:
[TestMethod]
public void Test()
{
var repository = new Mock<ICommonOperationsRepository>();
var deviceDetection = new Mock<IDeviceDetection>();
var controller = new DemoController(repository.Object, deviceDetection.Object);
controller.Default();
deviceDetection.Verify(x => x.DetectDevice(), Times.Once());
}