Azure Blob文件上传功能单元/集成测试编写方法咨询
Azure Blob上传方法测试实现方案
你现有的代码直接耦合了Azure Storage SDK的具体实现,没有做依赖隔离,纯单元测试没法直接写,分单元测试、集成测试两种场景的实现方式如下:
单元测试实现
单元测试的核心是隔离外部Blob存储依赖,只验证接口本身的参数校验、返回逻辑是否正确,首先要做最小程度的代码重构,不然没法Mock外部依赖:
- 把Blob上传的核心逻辑抽成接口服务,通过依赖注入注入到控制器,不要在接口方法里直接写连接串解析、Blob客户端初始化的逻辑
- 把参数校验逻辑前置,提前拦截无效请求
重构后的控制器代码参考:
// 先定义Blob操作抽象接口 public interface IBlobStorageService { Task<string> UploadAsync(Stream fileStream, string fileName); } // 你的原有Blob上传逻辑移到这个接口的实现类里,注册到DI容器 public class AzureBlobStorageService : IBlobStorageService { private readonly string _connectionString; private readonly string _containerName; public AzureBlobStorageService(IConfiguration config) { _connectionString = config.GetValue<string>("BlobConnectionString"); _containerName = config.GetValue<string>("BlobContainerName"); } public async Task<string> UploadAsync(Stream fileStream, string fileName) { CloudStorageAccount cloudStorageAccount = CloudStorageAccount.Parse(_connectionString); CloudBlobClient blobClient = cloudStorageAccount.CreateCloudBlobClient(); CloudBlobContainer container = blobClient.GetContainerReference(_containerName); // 可以加容器不存在时自动创建的逻辑,避免运行时报错 await container.CreateIfNotExistsAsync(); CloudBlockBlob blockBlob = container.GetBlockBlobReference(fileName); await blockBlob.UploadFromStreamAsync(fileStream); return blockBlob.Uri.OriginalString; } } // 控制器改造后 [ApiController] [Route("api/[controller]")] public class FileController : ControllerBase { private readonly IBlobStorageService _blobStorageService; public FileController(IBlobStorageService blobStorageService) { _blobStorageService = blobStorageService; } [HttpPost("upload")] public async Task<IActionResult> UploadFile(IFormFile files) { if (files == null || files.Length == 0) return BadRequest("请上传有效文件"); await using var stream = files.OpenReadStream(); var fileUri = await _blobStorageService.UploadAsync(stream, files.FileName); return Ok(fileUri); } }
重构完成后,就可以用Moq之类的Mock框架模拟Blob服务,写不依赖真实Azure资源的单元测试,参考用例(xUnit框架):
public class FileControllerTests { [Fact] public async Task Upload_ValidFile_ReturnsCorrectUri() { // 构造Mock的Blob服务 var mockBlobService = new Mock<IBlobStorageService>(); var testExpectedUri = "https://test-storage.blob.core.windows.net/container/test.jpg"; mockBlobService.Setup(s => s.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>())) .ReturnsAsync(testExpectedUri); var controller = new FileController(mockBlobService.Object); // 构造模拟上传文件 using var testStream = new MemoryStream(Encoding.UTF8.GetBytes("test content")); var testFormFile = new FormFile(testStream, 0, testStream.Length, "files", "test.jpg") { Headers = new HeaderDictionary(), ContentType = "image/jpeg" }; // 执行上传方法 var result = await controller.UploadFile(testFormFile); // 校验结果 var okResult = Assert.IsType<OkObjectResult>(result); Assert.Equal(testExpectedUri, okResult.Value); // 确认上传方法确实被调用 mockBlobService.Verify(s => s.UploadAsync(It.IsAny<Stream>(), "test.jpg"), Times.Once); } [Fact] public async Task Upload_EmptyFile_ReturnsBadRequest() { var mockBlobService = new Mock<IBlobStorageService>(); var controller = new FileController(mockBlobService.Object); var result = await controller.UploadFile(null); Assert.IsType<BadRequestObjectResult>(result); // 空文件不应该触发上传逻辑 mockBlobService.Verify(s => s.UploadAsync(It.IsAny<Stream>(), It.IsAny<string>()), Times.Never); } }
注:如果你坚持不重构代码,旧版WindowsAzure.Storage包里的CloudBlobClient等类没有对应抽象接口,没法直接Mock,只能写集成测试,单元测试无法覆盖核心逻辑。
集成测试实现
集成测试用来验证从接口请求到Blob真实上传的全链路逻辑,不需要隔离外部依赖,推荐优先用本地Azurite存储模拟器做测试,不需要消耗Azure云资源,也不会影响生产数据。
测试实现参考(用ASP.NET Core自带的WebApplicationFactory搭建测试宿主):
public class BlobUploadIntegrationTests : IClassFixture<WebApplicationFactory<Program>> { private readonly WebApplicationFactory<Program> _testFactory; public BlobUploadIntegrationTests(WebApplicationFactory<Program> factory) { // 替换测试环境配置,指向本地Azurite模拟器 _testFactory = factory.WithWebHostBuilder(builder => { builder.ConfigureAppConfiguration((ctx, cfg) => { cfg.AddInMemoryCollection(new Dictionary<string, string> { ["BlobConnectionString"] = "UseDevelopmentStorage=true", // Azurite默认连接串 ["BlobContainerName"] = "test-upload-container" }); }); }); } [Fact] public async Task Upload_FullPipeline_ReturnsAccessibleBlobUri() { var httpClient = _testFactory.CreateClient(); // 构造文件上传请求 using var formContent = new MultipartFormDataContent(); var testFileBytes = new byte[] { 0x01, 0x02, 0x03 }; // 模拟测试图片二进制内容 var fileContent = new ByteArrayContent(testFileBytes); fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/png"); formContent.Add(fileContent, "files", "test-upload.png"); // 调用上传接口 var response = await httpClient.PostAsync("/api/file/upload", formContent); response.EnsureSuccessStatusCode(); var returnedUri = await response.Content.ReadAsStringAsync(); // 校验返回结果 Assert.True(Uri.IsWellFormedUriString(returnedUri, UriKind.Absolute)); Assert.Contains("test-upload.png", returnedUri); // 可选:访问返回的URI,校验文件内容和上传的一致 var blobClient = new HttpClient(); var uploadedBytes = await blobClient.GetByteArrayAsync(returnedUri); Assert.Equal(testFileBytes, uploadedBytes); } }
注意事项:
- 本地跑集成测试需要先启动Azurite模拟器,VS安装Azure开发负载后会自带这个工具
- 如果用真实Azure存储账号做测试,必须用和生产隔离的专用测试账号,不要在测试代码里写生产环境连接串
- 测试跑完建议加清理逻辑,删除测试过程中上传的临时文件,避免冗余存储占用
内容的提问来源于stack exchange,提问作者EdwinThePower
相关产品推荐
相关产品推荐

