如何编写XUnit单元测试实现Azure Blob上传图片并获取绝对URL
Azure Blob 图片上传功能的XUnit单元测试实现
前置说明
直接对耦合真实Azure存储服务的代码编写单元测试会存在依赖网络、测试不稳定、产生云资源费用的问题,写测试前先对原有业务代码做可测试性重构,再通过依赖隔离编写稳定的单元用例。
另外你贴的原有代码存在两个明显问题:
- 所有上传文件都使用固定Blob名
images,后续上传会覆盖之前的文件 - 最后返回的
blob.AbsoluteURI是无效写法,代码中没有定义blob变量,且C# SDK中对应属性为Uri类型
第一步:重构业务代码适配单元测试
先抽离存储操作的服务层,把硬编码的配置、路径移除,通过依赖注入传入配置,同时把同步IO操作改成异步方法避免线程阻塞:
首先定义服务接口:
public interface IBlobUploadService { /// <summary> /// 上传本地图片到Blob存储 /// </summary> /// <param name="localFilePath">本地图片绝对路径</param> /// <param name="blobName">存储中Blob的唯一名称</param> /// <returns>上传后资源的绝对URI</returns> Task<Uri> UploadImageAsync(string localFilePath, string blobName); }
实现具体服务逻辑:
public class BlobUploadService : IBlobUploadService { private readonly string _storageConnectionString; private const string ContainerName = "myblob"; public BlobUploadService(IConfiguration configuration) { _storageConnectionString = configuration.GetConnectionString("AzureBlobStorage") ?? throw new ArgumentNullException("Azure Blob存储连接字符串未配置"); } public async Task<Uri> UploadImageAsync(string localFilePath, string blobName) { // 参数校验 if (!File.Exists(localFilePath)) throw new FileNotFoundException("指定图片文件不存在", localFilePath); if (string.IsNullOrWhiteSpace(blobName)) throw new ArgumentException("Blob名称不能为空", nameof(blobName)); // 初始化Blob客户端 CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_storageConnectionString); CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); CloudBlobContainer container = blobClient.GetContainerReference(ContainerName); await container.CreateIfNotExistsAsync(); // 上传文件 CloudBlockBlob blockBlob = container.GetBlockBlobReference(blobName); using var fileStream = File.OpenRead(localFilePath); await blockBlob.UploadFromStreamAsync(fileStream); return blockBlob.Uri; } }
如果你使用的是旧版
WindowsAzure.StorageNuget包,SDK类的方法多为非虚方法无法直接Mock,建议升级到新版Azure.Storage.Blobs包,或者自行封装一层薄的存储操作抽象做隔离。
第二步:编写XUnit单元测试
测试项目需要提前安装Nuget包:xunit、Moq、Microsoft.Extensions.Configuration.Abstractions。
单元测试需要覆盖核心场景,所有外部依赖通过Mock隔离,不连接真实Azure存储:
- 参数非法时抛出对应异常
- 容器不存在时自动创建
- 正常上传后返回格式正确的绝对URI
测试代码如下:
public class BlobUploadServiceTests { private readonly Mock<IConfiguration> _mockConfiguration; private const string FakeConnectionString = "DefaultEndpointsProtocol=https;AccountName=test;AccountKey=fakeKey;EndpointSuffix=core.windows.net"; public BlobUploadServiceTests() { _mockConfiguration = new Mock<IConfiguration>(); _mockConfiguration.Setup(c => c.GetConnectionString("AzureBlobStorage")) .Returns(FakeConnectionString); } [Fact] public async Task UploadImageAsync_FileNotExist_ThrowsFileNotFoundException() { // Arrange var service = new BlobUploadService(_mockConfiguration.Object); string nonExistPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.jpg"); // Act & Assert await Assert.ThrowsAsync<FileNotFoundException>(() => service.UploadImageAsync(nonExistPath, "test.jpg")); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null)] public async Task UploadImageAsync_InvalidBlobName_ThrowsArgumentException(string invalidBlobName) { // Arrange var service = new BlobUploadService(_mockConfiguration.Object); string tempFile = Path.GetTempFileName(); await File.WriteAllBytesAsync(tempFile, new byte[] { 0xFF, 0xD8, 0xFF }); // 模拟jpg文件头 try { // Act & Assert await Assert.ThrowsAsync<ArgumentException>(() => service.UploadImageAsync(tempFile, invalidBlobName)); } finally { File.Delete(tempFile); } } [Fact] public async Task UploadImageAsync_ValidInput_ReturnsCorrectAbsoluteUri() { // Arrange // 纯单元测试场景下可以进一步Mock Blob客户端的操作,完全隔离SDK依赖 // 若要验证真实上传逻辑,可以替换为测试存储账号/本地Azurite模拟器的连接字符串做集成测试 var service = new BlobUploadService(_mockConfiguration.Object); string tempFile = Path.GetTempFileName(); string testBlobName = $"{Guid.NewGuid()}.jpg"; await File.WriteAllBytesAsync(tempFile, new byte[] { 0xFF, 0xD8, 0xFF }); try { // Act Uri result = await service.UploadImageAsync(tempFile, testBlobName); // Assert Assert.True(result.IsAbsoluteUri); Assert.Contains(ContainerName, result.AbsolutePath); Assert.EndsWith(testBlobName, result.AbsolutePath); } finally { File.Delete(tempFile); } } }
补充建议
- 集成测试建议使用本地Azurite存储模拟器或者专用的测试存储账号,测试执行完成后自动清理测试产生的Blob资源,避免产生费用。
- 生产环境代码建议给Blob名称增加唯一前缀(比如Guid、时间戳),避免同名文件覆盖。
- 上传大文件时建议加上分块上传、异常重试逻辑,对应逻辑也需要补充单元测试覆盖。
内容的提问来源于stack exchange,提问作者Belphegor
相关产品推荐
相关产品推荐

