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

如何在C#中使用Moq对multipart/form-data进行单元测试

问题

我正在为一个从Request获取multipart/form-data的服务方法编写单元测试,业务方法代码如下:

var boundary = MultipartRequestHelper.GetBoundary(
                   MediaTypeHeaderValue.Parse(HttpContextAccessor.HttpContext.Request.ContentType),
                   DEFAULT_MULTIPART_BOUNDARY_LENGTH_LIMIT);

var reader = new MultipartReader(boundary, HttpContextAccessor.HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
var streamedFileContent = Array.Empty<byte>();
var dictionary = new Dictionary<string, string>();
while (section != null)
{
    var hasContentDispositionHeader =
    ContentDispositionHeaderValue.TryParse(
        section.ContentDisposition, out var contentDisposition);

    if (hasContentDispositionHeader)
    {
        if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
        {
            streamedFileContent = await FileHelpers.ProcessStreamedFile(
            section, contentDisposition, Configuration.GetFileSizeLimit());
        }
        else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
        {
            var key = HeaderUtilities
                 .RemoveQuotes(contentDisposition.Name).Value;
            var encoding = GetEncoding(section);

            if (encoding == null)
                throw new ErrorCodeException("error-invalid-data");

            using (var streamReader = new StreamReader(
                section.Body,
                encoding,
                detectEncodingFromByteOrderMarks: true,
                bufferSize: 1024,
                leaveOpen: true))
            {
                var value = await streamReader.ReadToEndAsync();
                dictionary.Add(key, value);
            }
        }
    }

    section = await reader.ReadNextSectionAsync();
}

var file = new File
{
    AuthorId = authorStaffId,
    Name = dictionary.FirstOrDefault(l => l.Key == "name").Value,
    Size = streamedFileContent.Length,
    Extension = dictionary.FirstOrDefault(l => l.Key == "extension").Value,
    MimeType = dictionary.FirstOrDefault(l => l.Key == "mimeType").Value,
    IsTemporary = true,
    FileId = Guid.NewGuid()
};
file = await SaveFileContentAsync(streamedFileContent, file);
file.ContentHash = CanculateContentHash(streamedFileContent);
await _fileRepository.AddAsync(file);

通过Postman发送请求时方法运行正常,但用xUnit和Moq测试时遇到两个问题:

第一种测试代码及报错

[Fact]
public async Task UploadFileAsync_WhenFileSizeIsNotBiggerThanLimit_Successful()
{
    // Arrange
    ConfigureClaims();
    ConfigureDatabase();
    ConfigureService();

    var fakeFileContent = GetFakeFileContent();
    var byteContent = Convert.FromBase64String(fakeFileContent); 

    Stream stream = new MemoryStream(byteContent);
    _moqHttpContextAccessor.Setup(x => x.HttpContext.Request.Body).Returns(stream);
    _moqHttpContextAccessor.Setup(x => x.HttpContext.Request.ContentType).Returns("multipart/form-data; boundary=----231179732646258011288433");
    _moqHttpContextAccessor.Setup(x => x.HttpContext.Request.ContentLength).Returns(0x000000000001d74e);

    // Act
    var addedFileId = _baseFileService.UploadFileAsync();
    var addedFile = _fileRepository.GetById(addedFileId);

    // Assert
    Assert.NotNull(addedFile);
}

运行报错:InvalidDataException: Multipart body length limit 16384 exceeded

第二种测试代码及报错

[Fact]
public async Task UploadFileAsync_WhenFileSizeIsNotBiggerThanLimit_Successful()
{
    // Arrange
    ConfigureClaims();
    ConfigureDatabase();
    ConfigureService();

    var fakeFileContent = GetFakeFileContent();
    var byteContent = Convert.FromBase64String(fakeFileContent);

    var byteStringContent = byteContent.ToString();
    var forDataDictionary = new Dictionary<string, StringValues>
    {
        {"file", byteStringContent},
        {"name", "test.pdf"},
        {"size", "120085"},
        {"mimeType", "application/pdf" }
    };
    var formData = new FormCollection(forDataDictionary);
    _moqHttpContextAccessor.Setup(x => x.HttpContext.Request.Form).Returns(formData);

    // Act
    var addedFileId = _baseFileService.UploadFileAsync();
    var addedFile = _fileRepository.GetById(addedFileId);

    // Assert
    Assert.NotNull(addedFile);
}

运行报错,原因是业务代码实际从Request.Body读取数据,而非Request.Form。

找不到合适的解决方案,该如何处理?

解决方案

问题1:Multipart body length limit 16384 exceeded

这个错误是因为MultipartReader默认的最大body长度限制是16KB,你的测试文件超过了这个阈值。有两种解决方式:

  • 修改业务代码:创建MultipartReader时传入自定义的长度限制(建议和你配置的文件大小限制保持一致):
var maxBodyLength = Configuration.GetFileSizeLimit();
var reader = new MultipartReader(boundary, HttpContextAccessor.HttpContext.Request.Body, maxBodyLength);
  • 在测试中配置请求限制:通过Moq设置Request.MultipartBodyLengthLimit为足够大的值:
_moqHttpContextAccessor.Setup(x => x.HttpContext.Request.MultipartBodyLengthLimit)
                       .Returns((long)1024 * 1024 * 10); // 设置为10MB,可根据实际调整

问题2:业务代码读取Request.Body而非Request.Form

第二种测试方式直接模拟Request.Form无效,因为业务代码是直接解析Request.Body中的multipart原始内容。正确做法是构造符合multipart/form-data格式的完整请求流,示例代码如下:

[Fact]
public async Task UploadFileAsync_WhenFileSizeIsNotBiggerThanLimit_Successful()
{
    // Arrange
    ConfigureClaims();
    ConfigureDatabase();
    ConfigureService();

    var boundary = "----231179732646258011288433";
    var fakeFileContent = GetFakeFileContent();
    var fileBytes = Convert.FromBase64String(fakeFileContent);

    // 构建完整的multipart请求流
    using var memoryStream = new MemoryStream();
    using var writer = new StreamWriter(memoryStream, Encoding.UTF8, 1024, leaveOpen: true);

    // 写入表单字段:name
    await writer.WriteLineAsync($"--{boundary}");
    await writer.WriteLineAsync("Content-Disposition: form-data; name=\"name\"");
    await writer.WriteLineAsync();
    await writer.WriteLineAsync("test.pdf");

    // 写入表单字段:extension
    await writer.WriteLineAsync($"--{boundary}");
    await writer.WriteLineAsync("Content-Disposition: form-data; name=\"extension\"");
    await writer.WriteLineAsync();
    await writer.WriteLineAsync(".pdf");

    // 写入表单字段:mimeType
    await writer.WriteLineAsync($"--{boundary}");
    await writer.WriteLineAsync("Content-Disposition: form-data; name=\"mimeType\"");
    await writer.WriteLineAsync();
    await writer.WriteLineAsync("application/pdf");

    // 写入文件part
    await writer.WriteLineAsync($"--{boundary}");
    await writer.WriteLineAsync("Content-Disposition: form-data; name=\"file\"; filename=\"test.pdf\"");
    await writer.WriteLineAsync("Content-Type: application/pdf");
    await writer.WriteLineAsync();
    await writer.FlushAsync(); // 确保写入流
    await memoryStream.WriteAsync(fileBytes, 0, fileBytes.Length);
    await writer.WriteLineAsync();
    await writer.WriteLineAsync($"--{boundary}--"); // 结束分隔符
    await writer.FlushAsync();

    memoryStream.Position = 0; // 重置流位置到开头

    // 模拟Request
    _moqHttpContextAccessor.Setup(x => x.HttpContext.Request.Body).Returns(memoryStream);
    _moqHttpContextAccessor.Setup(x => x.HttpContext.Request.ContentType)
                           .Returns($"multipart/form-data; boundary={boundary}");
    _moqHttpContextAccessor.Setup(x => x.HttpContext.Request.ContentLength).Returns(memoryStream.Length);
    // 设置MultipartBodyLengthLimit避免长度限制错误
    _moqHttpContextAccessor.Setup(x => x.HttpContext.Request.MultipartBodyLengthLimit)
                           .Returns((long)1024 * 1024 * 10);

    // Act
    var addedFileId = await _baseFileService.UploadFileAsync(); // 必须await异步方法
    var addedFile = _fileRepository.GetById(addedFileId);

    // Assert
    Assert.NotNull(addedFile);
    Assert.Equal("test.pdf", addedFile.Name);
    Assert.Equal(fileBytes.Length, addedFile.Size);
}

另外注意:之前的测试代码调用UploadFileAsync()时未加await,会导致异步方法未执行完成就去获取文件,这是潜在问题,必须补上await。

内容的提问来源于stack exchange,提问作者Niyozbek Mirzayev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 11:20:28