.NET 6中用MOQ+Xunit模拟含RouteData.Values的Controller Action遇空引用
问题:测试Controller Action时RouteData.Values抛出空引用异常
我正在尝试模拟一个使用RouteData.Values的Controller Action方法,运行测试时,Action方法中var countryCode = (string)this.RouteData.Values["CountryCode"]行抛出"Object reference not set to an instance of an object."空引用异常。相关代码如下:
待测试的Action方法
public IActionResult Get(long siteId) { //Get all the Reports var countryCode = (string)this.RouteData.Values["CountryCode"]; var microMarketReport = _microMarketPriceOverviewBusiness.GetMicroMarketOverviewReport(siteId, countryCode).GetAwaiter().GetResult(); return Ok(microMarketReport); }
测试方法
[Fact] public void Get_OK() { // Arrange MockMicroMarketPriceOverviewBusiness.Setup(x => x.GetMicroMarketOverviewReport(It.IsAny<long>(),It.IsAny<string>())) .Returns(Task.Run(() => new MicroMarketOverviewModel())); var unitUnderTest = CreateMicroMarketPriceOverviewController(); var routeData = new RouteData(); routeData.Values.Add("CountryCode", "DE"); // Act var result = (OkObjectResult)unitUnderTest.Get(120); // Assert Assert.True(result.StatusCode == StatusCodes.Status200OK); }
创建Controller的方法
private MicroMarketPriceOverviewController CreateMicroMarketPriceOverviewController() { var context = new DefaultHttpContext(); context.Request.Headers.Add("X-Correlation-ID", Guid.NewGuid().ToString()); var controllerContext = new ControllerContext() { HttpContext = context.HttpContext }; context.HttpContext.Request.Path = PathString.FromUriComponent(new Uri("http://localhost/api/v1/{CountryCode}/[controller]")); context.HttpContext.Request.RouteValues = new Microsoft.AspNetCore.Routing.RouteValueDictionary() { { "CountryCode", "NL" } }; return new MicroMarketPriceOverviewController(MockMicroMarketPriceOverviewBusiness.Object, mockLogger); }
问题原因及解决方案
核心问题:Controller的RouteData未被正确关联
Controller的RouteData属性属于ControllerContext的一部分,当前代码存在两个关键错误:
- 在
CreateMicroMarketPriceOverviewController方法中,创建了controllerContext但未赋值给新建的Controller实例; - 测试方法中创建的
routeData对象,没有关联到Controller的ControllerContext中。
修正方案
方案一:创建Controller时直接设置RouteData
修改CreateMicroMarketPriceOverviewController方法,将RouteData关联到ControllerContext并赋值给Controller:
private MicroMarketPriceOverviewController CreateMicroMarketPriceOverviewController() { var context = new DefaultHttpContext(); context.Request.Headers.Add("X-Correlation-ID", Guid.NewGuid().ToString()); // 初始化RouteData并添加参数 var routeData = new RouteData(); routeData.Values.Add("CountryCode", "NL"); // 配置ControllerContext并关联RouteData var controllerContext = new ControllerContext() { HttpContext = context.HttpContext, RouteData = routeData }; // 创建Controller并赋值ControllerContext var controller = new MicroMarketPriceOverviewController(MockMicroMarketPriceOverviewBusiness.Object, mockLogger); controller.ControllerContext = controllerContext; return controller; }
方案二:测试方法中动态设置RouteData
如果需要在测试中灵活切换CountryCode,可以直接修改Controller的ControllerContext.RouteData:
[Fact] public void Get_OK() { // Arrange MockMicroMarketPriceOverviewBusiness.Setup(x => x.GetMicroMarketOverviewReport(It.IsAny<long>(),It.IsAny<string>())) .Returns(Task.Run(() => new MicroMarketOverviewModel())); var unitUnderTest = CreateMicroMarketPriceOverviewController(); // 初始化RouteData并关联到Controller var routeData = new RouteData(); routeData.Values.Add("CountryCode", "DE"); unitUnderTest.ControllerContext.RouteData = routeData; // Act var result = (OkObjectResult)unitUnderTest.Get(120); // Assert Assert.True(result.StatusCode == StatusCodes.Status200OK); }
同时需确保CreateMicroMarketPriceOverviewController方法初始化Controller的ControllerContext:
private MicroMarketPriceOverviewController CreateMicroMarketPriceOverviewController() { var context = new DefaultHttpContext(); context.Request.Headers.Add("X-Correlation-ID", Guid.NewGuid().ToString()); var controller = new MicroMarketPriceOverviewController(MockMicroMarketPriceOverviewBusiness.Object, mockLogger); // 初始化ControllerContext controller.ControllerContext = new ControllerContext() { HttpContext = context.HttpContext }; return controller; }
额外优化:避免同步调用异步方法
Action中使用.GetAwaiter().GetResult()同步调用异步方法可能引发死锁,建议改为异步版本:
public async Task<IActionResult> Get(long siteId) { var countryCode = (string)this.RouteData.Values["CountryCode"]; var microMarketReport = await _microMarketPriceOverviewBusiness.GetMicroMarketOverviewReport(siteId, countryCode); return Ok(microMarketReport); }
对应的测试方法同步修改:
[Fact] public async Task Get_OK() { // Arrange MockMicroMarketPriceOverviewBusiness.Setup(x => x.GetMicroMarketOverviewReport(It.IsAny<long>(),It.IsAny<string>())) .ReturnsAsync(new MicroMarketOverviewModel()); var unitUnderTest = CreateMicroMarketPriceOverviewController(); var routeData = new RouteData(); routeData.Values.Add("CountryCode", "DE"); unitUnderTest.ControllerContext.RouteData = routeData; // Act var result = (OkObjectResult)await unitUnderTest.Get(120); // Assert Assert.Equal(StatusCodes.Status200OK, result.StatusCode); }
内容的提问来源于stack exchange,提问作者Abhishek_Singh_Rana
相关产品推荐
相关产品推荐

