You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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;
}

问题原因分析

  1. 固定的Form实例:构造函数中仅Setup了一次Request.Form返回初始的requestForm,后续AddFormFieldsOrFile中新建的requestForm没有重新绑定到Mock,导致测试代码拿到的始终是初始空表单。
  2. 多余的Keys Mock:单独MockForm.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();
    }
}

修复说明

  1. 移除了对Form.Keys和Form.Files的单独Mock,直接依赖FormCollection自身的实现。
  2. 保存DefaultHttpContext实例,修改Headers时更直接,避免频繁从Mock中获取。
  3. 新增UpdateFormSetup方法,在新建requestForm后重新SetupRequest.Form,确保测试代码能拿到最新的表单实例。
  4. 移除冗余的keys列表,不再手动维护键集合。

内容的提问来源于stack exchange,提问作者geoff swartz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 11:57:34