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

.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 19:34:52