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

