.NET 6单元测试Mock HttpContext时表单字段无法获取的问题
.NET 6 Web应用单元测试Mock HttpContext表单字段空引用异常解决
问题描述
为.NET 6 Web应用编写单元测试时Mock了HttpContext,能正常获取FileCollection中的文件,但访问表单字段时抛出「Object reference not set to an instance of an object」空引用异常。从Mock对象可见Files集合有1个文件,但表单字段无法正常访问。
测试代码
using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Moq; using NUnit.Framework; using TestWebApp.Controllers; namespace TestDemo; [TestFixture] public class TestControllerFixture { [Test] public void returns_sum() { var content = "Hello World from a Fake File"; var fileName = "test.pdf"; var stream = new MemoryStream(); var writer = new StreamWriter(stream); writer.Write(content); writer.Flush(); stream.Position = 0; IFormFile file = new FormFile(stream, 0, stream.Length, "fileId", fileName); var _httpContextAccessor = new MockHttpContextAccessor(); _httpContextAccessor.AddFormFieldsOrFile(new Dictionary<string, StringValues>() { {"expense-ids", new StringValues(new[] {"1","2","3"})} }, new List<IFormFile>() {file}); var controller = new TestController(_httpContextAccessor.ContextAccessor.Object); var result = controller.Test(); Assert.AreEqual(3, result); } } public class MockHttpContextAccessor { public Mock<IHttpContextAccessor> ContextAccessor { get; private set; } private FormFileCollection fileCollection; private FormCollection requestForm; public MockHttpContextAccessor() { var requestFeature = new HttpRequestFeature(); var features = new FeatureCollection(); features.Set<IHttpRequestFeature>(requestFeature); ContextAccessor = new Mock<IHttpContextAccessor>(); var session = new MockHttpSession(); var context = new DefaultHttpContext(features); fileCollection = new FormFileCollection(); requestForm = new FormCollection(new Dictionary<string, StringValues>(), fileCollection); ContextAccessor.Setup(x => x.HttpContext).Returns(context); ContextAccessor.Setup(x => x.HttpContext.Session).Returns(session); ContextAccessor.Setup(x => x.HttpContext.Request.Path).Returns("/path"); ContextAccessor.Setup(x => x.HttpContext.Request.Form).Returns(requestForm); ContextAccessor.Setup(x => x.HttpContext.Request.Form.Files).Returns(requestForm.Files); ContextAccessor.Setup(x => x.HttpContext.Request.Headers).Returns(new Mock<IHeaderDictionary>().Object); context.Request.Form = requestForm; } public void AddRequestBody(string content) { ContextAccessor.Setup(x => x.HttpContext.Request.Body) .Returns(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(content))); } public void AddFormFieldsOrFile(Dictionary<string, StringValues> fields, List<IFormFile> files = null) { if (files != null && files.Any()) { ContextAccessor.Object.HttpContext.Request.Headers.Add("Content-Type", "multipart/form-data"); fileCollection.AddRange(files); } requestForm = new FormCollection(fields, fileCollection); } public void ClearFormFiles() { fileCollection.Clear(); } } public class MockHttpSession : ISession { Dictionary<string, object> sessionStorage = new Dictionary<string, object>(); public object this[string name] { get { return sessionStorage[name]; } set { sessionStorage[name] = value; } } string ISession.Id { get { throw new NotImplementedException(); } } bool ISession.IsAvailable { get { throw new NotImplementedException(); } } IEnumerable<string> ISession.Keys { get { return sessionStorage.Keys; } } void ISession.Clear() { sessionStorage.Clear(); } void ISession.Remove(string key) { sessionStorage.Remove(key); } void ISession.Set(string key, byte[] value) { sessionStorage[key] = value; } public Task LoadAsync(CancellationToken cancellationToken = new CancellationToken()) { throw new NotImplementedException(); } public Task CommitAsync(CancellationToken cancellationToken = new CancellationToken()) { throw new NotImplementedException(); } bool ISession.TryGetValue(string key, out byte[] value) { object storageValue = null; if (sessionStorage.TryGetValue(key, out storageValue)) { var bytes = storageValue as byte[]; if (bytes != null) { value = bytes; return true; } value = Encoding.UTF8.GetBytes(storageValue.ToString()); return true; } value = null; return false; } }
被测试的控制器方法
using Microsoft.AspNetCore.Mvc; namespace TestWebApp.Controllers; public class TestController : Controller { private IHttpContextAccessor _httpContextAccessor; public TestController(IHttpContextAccessor context) { _httpContextAccessor = context; } [HttpPost("[action]")] public IActionResult Test() { var form = _httpContextAccessor.HttpContext.Request.Form; var file = form.Files[0]; var ids = form["expense-ids"].ToString().Split(',') .Select(int.Parse).ToList(); var total = ids.Sum(); return Json(new {total}); } }
异常堆栈信息
at System.Collections.Generic.LargeArrayBuilder`1.AddRange(IEnumerable`1 items) at System.Collections.Generic.EnumerableHelpers.ToArray[T](IEnumerable`1 source) at System.Linq.Enumerable.ToArray[TSource](IEnumerable`1 source) at System.Linq.SystemCore_EnumerableDebugView`1.get_Items()
问题根源
在MockHttpContextAccessor的AddFormFieldsOrFile方法中,虽然创建了包含新表单字段的FormCollection并赋值给requestForm,但没有将这个新的FormCollection同步到HttpContext.Request.Form中,同时也没有更新Moq的Setup配置,导致控制器访问的还是初始的空表单集合,从而引发空引用异常。
解决方案
修改AddFormFieldsOrFile方法,在创建新的FormCollection后,更新HttpContext.Request.Form并重新设置Moq的Setup:
public void AddFormFieldsOrFile(Dictionary<string, StringValues> fields, List<IFormFile> files = null) { if (files != null && files.Any()) { ContextAccessor.Object.HttpContext.Request.Headers.Add("Content-Type", "multipart/form-data"); fileCollection.AddRange(files); } requestForm = new FormCollection(fields, fileCollection); // 更新HttpContext的Request.Form ContextAccessor.Object.HttpContext.Request.Form = requestForm; // 重新设置Moq的Setup,让Request.Form返回新的表单集合 ContextAccessor.Setup(x => x.HttpContext.Request.Form).Returns(requestForm); }
这样控制器就能访问到正确的表单字段了,测试也能正常通过。
内容的提问来源于stack exchange,提问作者geoff swartz
相关产品推荐
相关产品推荐

