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

C#项目中如何单元测试Amazon S3文件上传逻辑

问题根源

你当前无法断言UploadAsync调用的核心原因是:UploadToS3方法内部直接通过new TransferUtility(_client)创建了上传工具实例,你在测试类中Mock的_transfer对象完全没有被被测系统(FileRepository)使用,两者没有任何关联,自然无法捕获调用记录。

另外你现有测试还有两个遗漏点:

  • 没有配置CheckFileExists方法的Mock返回值,默认逻辑会走到"文件已存在"的分支,根本不会触发上传流程
  • 没有实际调用被测的PutFileInStorage/UploadToS3方法,断言没有对应的执行上下文

推荐实现方案(可维护性最高)

通过依赖注入改造生产代码,将TransferUtility的抽象依赖注入,而不是在方法内部直接new实例,这样就可以直接Mock上传行为做断言。

第一步:改造生产代码

不要在业务方法内部直接初始化TransferUtility,改为构造函数注入:

private readonly IAmazonS3 _client;
private readonly ITransferUtility _transferUtility; // 注意用接口,不要用具体类

// 构造函数注入依赖
public FileRepository(IAmazonS3 client, ITransferUtility transferUtility)
{
    _client = client;
    _transferUtility = transferUtility;
}

public async Task PutFileInStorage(string fileName, string content)
{      
    try
    {
        await UploadToS3(fileName, content);
    }
    catch (Exception ex)
    {
        throw new Exception(ex.Message);
    }
}
 
public async Task UploadToS3(string fileName, string content)
{            
    using MemoryStream ms = new(Encoding.UTF8.GetBytes(content));
         
    var uploadRequest = new TransferUtilityUploadRequest
    {
        InputStream = ms,
        Key = fileName,
        BucketName = "aws-s3-apadmi"                    
    };
    
    bool fileCheck = await CheckFileExists(uploadRequest.Key, uploadRequest.BucketName);
    
    if (fileCheck)
        throw new Exception("File already exists");                

    // 直接用注入的实例上传,不要new
    try
    {
        await _transferUtility.UploadAsync(uploadRequest);
    }
    catch (Exception ex)
    {
        throw new Exception(ex.Message);
    }
}

第二步:编写对应单元测试

测试中直接Mock注入的ITransferUtility,配置依赖返回值,调用被测方法后断言调用次数即可:

using Amazon.S3;
using Amazon.S3.Model;
using Amazon.S3.Transfer;
using AmazonStorage.Repositories;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace AmazonStorage.Tests.Repositories
{
    [TestClass]
    public class FileRepositoryTests
    {
        private IAmazonS3 _client;
        private ITransferUtility _transferUtility;
        private FileRepository _sut;

        [TestInitialize] // 用测试初始化方法,不要在构造函数里初始化测试依赖
        public void Setup()
        {
            _client = Substitute.For<IAmazonS3>();
            _transferUtility = Substitute.For<ITransferUtility>();
            _sut = new FileRepository(_client, _transferUtility);
        }

        [TestMethod]        
        public async Task PutFileInStorage_WhenFileNotExists_ShouldCallUploadAsyncOnce()
        {   
            // Arrange
            string testFileName = "test.txt";
            string testContent = "test content";
            // 配置文件不存在的Mock返回,让逻辑走到上传分支
            _client.GetObjectAsync(Arg.Any<GetObjectRequest>(), Arg.Any<CancellationToken>())
                .Returns<Task<GetObjectResponse>>(_ => throw new AmazonS3Exception("not found"));
            // 注:如果CheckFileExists是独立封装的可直接Mock的方法,直接配置其返回false即可

            // Act
            await _sut.PutFileInStorage(testFileName, testContent);
       
            //Assert
            // 验证UploadAsync被恰好调用1次,且入参符合预期
            await _transferUtility.Received(1).UploadAsync(Arg.Is<TransferUtilityUploadRequest>(req =>
                req.Key == testFileName &&
                req.BucketName == "aws-s3-apadmi" &&
                req.InputStream != null
            ), Arg.Any<CancellationToken>());
        }

        [TestMethod]
        public async Task PutFileInStorage_WhenFileAlreadyExists_ShouldThrowErrorAndNotCallUpload()
        {
            // Arrange
            string testFileName = "exists.txt";
            string testContent = "test content";
            // 配置文件存在的返回
            _client.GetObjectAsync(Arg.Any<GetObjectRequest>(), Arg.Any<CancellationToken>())
                .Returns(new GetObjectResponse());

            // Act & Assert
            var ex = await Assert.ThrowsExceptionAsync<Exception>(() => _sut.PutFileInStorage(testFileName, testContent));
            Assert.AreEqual("File already exists", ex.Message);
            // 验证上传方法一次都没被调用
            await _transferUtility.DidNotReceive().UploadAsync(Arg.Any<TransferUtilityUploadRequest>(), Arg.Any<CancellationToken>());
        }
    }
}

不改生产代码的替代方案

如果你暂时不想做依赖注入改造,因为TransferUtility本质是调用底层IAmazonS3的PutObject接口完成上传,你可以直接断言IAmazonS3的PutObjectAsync方法被调用即可,不需要Mock TransferUtility:

注意:这种方式和TransferUtility的内部实现耦合,如果后续AWS SDK升级改了TransferUtility的底层调用逻辑,测试会失效,优先用上面的依赖注入方案。

[TestMethod]        
public async Task PutFileInStorage_WhenFileNotExists_ShouldCallS3PutOnce()
{   
    // Arrange
    _client = Substitute.For<IAmazonS3>();
    _sut = new FileRepository(_client);
    string testFileName = "test.txt";
    string testContent = "test content";
    // 配置文件不存在
    _client.GetObjectAsync(Arg.Any<GetObjectRequest>(), Arg.Any<CancellationToken>())
        .Returns<Task<GetObjectResponse>>(_ => throw new AmazonS3Exception("not found"));

    // Act
    await _sut.PutFileInStorage(testFileName, testContent);

    // Assert
    // 验证底层S3客户端的上传方法被调用1次
    await _client.Received(1).PutObjectAsync(Arg.Is<PutObjectRequest>(req =>
        req.Key == testFileName &&
        req.BucketName == "aws-s3-apadmi"
    ), Arg.Any<CancellationToken>());
}

额外注意点
  • 测试类的依赖初始化不要写在构造函数里,用MSTest自带的[TestInitialize]标注的方法初始化,避免测试用例之间的状态污染
  • 断言的时候不要只验证方法被调用,最好同时验证传入的请求参数(Key、桶名、内容流)符合预期,避免逻辑写错传了错误的参数
  • 异常分支也要写测试用例,覆盖文件已存在、上传抛错等场景

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 18:57:18