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

如何编写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.Storage Nuget包,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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 23:48:22