如何在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
相关产品推荐
相关产品推荐

