.NET 6控制器测试中如何正确模拟带Request.Form的HttpContextAccessor
模拟IHttpContextAccessor时表单Keys集合遍历失效问题
我编写了用于模拟IHttpContextAccessor的MockHttpContextAccessor类,大部分功能运行正常,但在遍历Keys集合获取表单值时无法正常工作。
在AddFormFieldsOrFile方法中创建新的requestForm时,调试可见该对象包含Count、Keys、Store等属性且已添加对应值,但在遍历Keys的测试代码断点处,Request.Form的模拟对象发生变化,Store属性消失,无法通过键访问对应值,最终返回空字符串。我推测是requestForm的模拟设置不正确。
问题代码:MockHttpContextAccessor
public class MockHttpContextAccessor { public Mock<IHttpContextAccessor> ContextAccessor { get; private set; } private FormFileCollection fileCollection; private FormCollection requestForm; private List<string> keys = new List<string>(); 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.Form.Keys).Returns(keys); ContextAccessor.Setup(x => x.HttpContext.Request.Headers).Returns(new Mock<IHeaderDictionary>().Object); } public void AddRequestBody(string content) { ContextAccessor.Setup(x => x.HttpContext.Request.Body).Returns(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(content))); } public void AddQueryStringParameters(Dictionary<string, string> p) { ContextAccessor.Setup(x => x.HttpContext.Request.QueryString).Returns(new QueryString("?" + string.Join("&", p.Select(x => $"{x.Key}={x.Value}")))); } public void ClearQueryStringParameters() { ContextAccessor .Setup(x => x.HttpContext.Request.QueryString) .Returns(new QueryString("?")); } public void SetDefaultFileUpload(string content = "some content", string filename = "filename.pdf", string formFieldID = "id_from_form") { 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, formFieldID, filename); var formContents = new Dictionary<string, StringValues>(); AddFormFieldsOrFile(formContents, new List<IFormFile> { file }); } 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); foreach (var field in fields) { keys.Add(field.Key); } } public void ClearFormFiles() { fileCollection.Clear(); } }
测试设置代码
[SetUp] public override async Task SetUp() { _httpContextAccessor.AddFormFieldsOrFile(new Dictionary<string, StringValues>() { {"workingWithAgency", new StringValues("66199")} }); await base.SetUp(); }
待测试代码
private List<SpecifiedByRecord> getFormFieldContacts(string name) { var retval = new List<SpecifiedByRecord>(); foreach (var key in _context.Request.Form.Keys) if (key.Contains(name)) { var val = _context.Request.Form[key].ToString(); var els = val.Split('^'); retval.Add(new SpecifiedByRecord {Id = els[0], Name = els[1]}); } return retval; }
问题原因分析
- 固定的Form实例:构造函数中仅Setup了一次
Request.Form返回初始的requestForm,后续AddFormFieldsOrFile中新建的requestForm没有重新绑定到Mock,导致测试代码拿到的始终是初始空表单。 - 多余的Keys Mock:单独Mock
Form.Keys为独立的List<string>,但这个列表和实际requestForm的Keys脱节,且FormCollection本身已经正确实现了Keys属性,无需额外Mock。
修复后的代码
public class MockHttpContextAccessor { public Mock<IHttpContextAccessor> ContextAccessor { get; private set; } private FormFileCollection fileCollection; private FormCollection requestForm; private DefaultHttpContext context; public MockHttpContextAccessor() { var requestFeature = new HttpRequestFeature(); var features = new FeatureCollection(); features.Set<IHttpRequestFeature>(requestFeature); ContextAccessor = new Mock<IHttpContextAccessor>(); var session = new MockHttpSession(); 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"); UpdateFormSetup(); ContextAccessor.Setup(x => x.HttpContext.Request.Headers).Returns(new Mock<IHeaderDictionary>().Object); } private void UpdateFormSetup() { ContextAccessor.Setup(x => x.HttpContext.Request.Form).Returns(requestForm); } public void AddRequestBody(string content) { ContextAccessor.Setup(x => x.HttpContext.Request.Body).Returns(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(content))); } public void AddQueryStringParameters(Dictionary<string, string> p) { ContextAccessor.Setup(x => x.HttpContext.Request.QueryString).Returns(new QueryString("?" + string.Join("&", p.Select(x => $"{x.Key}={x.Value}")))); } public void ClearQueryStringParameters() { ContextAccessor .Setup(x => x.HttpContext.Request.QueryString) .Returns(new QueryString("?")); } public void SetDefaultFileUpload(string content = "some content", string filename = "filename.pdf", string formFieldID = "id_from_form") { 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, formFieldID, filename); var formContents = new Dictionary<string, StringValues>(); AddFormFieldsOrFile(formContents, new List<IFormFile> { file }); } public void AddFormFieldsOrFile(Dictionary<string, StringValues> fields, List<IFormFile> files = null) { if (files != null && files.Any()) { context.Request.Headers.Add("Content-Type", "multipart/form-data"); fileCollection.AddRange(files); } requestForm = new FormCollection(fields, fileCollection); UpdateFormSetup(); } public void ClearFormFiles() { fileCollection.Clear(); requestForm = new FormCollection(new Dictionary<string, StringValues>(), fileCollection); UpdateFormSetup(); } }
修复说明
- 移除了对
Form.Keys和Form.Files的单独Mock,直接依赖FormCollection自身的实现。 - 保存
DefaultHttpContext实例,修改Headers时更直接,避免频繁从Mock中获取。 - 新增
UpdateFormSetup方法,在新建requestForm后重新SetupRequest.Form,确保测试代码能拿到最新的表单实例。 - 移除冗余的
keys列表,不再手动维护键集合。
内容的提问来源于stack exchange,提问作者geoff swartz
相关产品推荐
相关产品推荐

