为密封类PngBitmapEncoder伪造流:单元测试I/O依赖后续问题
嘿,我来帮你搞定这个测试问题!这确实是《带有I/O依赖的单元测试》的典型后续场景——要验证PngBitmapEncoder的Save调用,同时避开真实的文件系统依赖。下面是用Rhino Mocks实现的完整方案:
核心思路
咱们要解决两个核心问题:
- 让
PngBitmapEncoder接受可控的模拟流(不用真实FileStream) - 明确断言
encoder.Save()方法确实被调用了
第一步:先把依赖抽象做对
首先,你的BitmapService不能直接硬编码new FileStream()或new PngBitmapEncoder()——得依赖抽象,这样才能Mock。比如:
1.1 完善你的IFile抽象
public interface IFile { // 返回包装好的流(适配你现有代码里的outStream.StreamInstance结构) StreamWrapper Create(string filePath); } // 对应你代码里的流包装类(如果没有可以直接返回Stream) public class StreamWrapper { public Stream StreamInstance { get; } public StreamWrapper(Stream stream) => StreamInstance = stream; }
1.2 抽象位图编码器(关键!不然没法断言Save调用)
因为PngBitmapEncoder是具体类,直接Mock它会很麻烦,咱们包一层抽象:
public interface IPngEncoder { void Save(Stream stream); BitmapFrameCollection Frames { get; } // 转发真实编码器的Frames属性 } // 真实实现,包装.NET自带的PngBitmapEncoder public class PngEncoderWrapper : IPngEncoder { private readonly PngBitmapEncoder _innerEncoder = new PngBitmapEncoder(); public void Save(Stream stream) => _innerEncoder.Save(stream); public BitmapFrameCollection Frames => _innerEncoder.Frames; }
1.3 改造BitmapService依赖抽象
public class BitmapService { private readonly IFile _fileProvider; private readonly IPngEncoder _pngEncoder; // 通过构造注入依赖,方便测试时传Mock public BitmapService(IFile fileProvider, IPngEncoder pngEncoder) { _fileProvider = fileProvider; _pngEncoder = pngEncoder; } public void SaveBitmapAsPngImage(BitmapSource bitmap, string filePath) { using var streamWrapper = _fileProvider.Create(filePath); _pngEncoder.Frames.Add(BitmapFrame.Create(bitmap)); _pngEncoder.Save(streamWrapper.StreamInstance); // 这就是你要断言的调用! } }
第二步:用Rhino Mocks写测试
现在可以轻松Mock所有依赖,验证行为:
[Test] public void BitmapService_Should_Call_PngEncoder_Save_When_Saving_Bitmap() { // Arrange var mockRepo = new MockRepository(); // 1. Mock IFile,返回可控的MemoryStream(替代真实FileStream) var fileMock = mockRepo.GenerateStrictMock<IFile>(); var testStream = new MemoryStream(); fileMock.Expect(f => f.Create(Arg<string>.Is.Anything)) .Return(new StreamWrapper(testStream)); // 2. Mock IPngEncoder,断言Save方法会被调用 var encoderMock = mockRepo.GenerateStrictMock<IPngEncoder>(); encoderMock.Expect(e => e.Save(Arg<Stream>.Is.Equal(testStream))) .Repeat.Once(); // 确保只调用一次 // 实例化服务,传入Mock var service = new BitmapService(fileMock, encoderMock); // 生成一个测试用的BitmapSource(不用真实图片) var testBitmap = BitmapSource.Create( width: 10, height: 10, dpiX: 96, dpiY: 96, format: PixelFormats.Bgr32, palette: null, pixels: new byte[10*10*4], stride: 10*4 ); // Act service.SaveBitmapAsPngImage(testBitmap, "dummy-path.png"); // Assert encoderMock.VerifyAllExpectations(); // 验证Save确实被调用了 fileMock.VerifyAllExpectations(); // 验证文件创建确实被调用了 // 可选:验证流有写入内容(说明编码器真的工作了) Assert.Greater(testStream.Length, 0); }
如果你不想抽象编码器的替代方案
如果不想额外加IPngEncoder,可以用Rhino Mocks的Partial Mock来拦截真实PngBitmapEncoder的Save方法:
[Test] public void BitmapService_Should_Call_Save_On_PngBitmapEncoder() { // Arrange var mockRepo = new MockRepository(); var fileMock = mockRepo.GenerateStrictMock<IFile>(); var testStream = new MemoryStream(); fileMock.Expect(f => f.Create(Arg<string>.Is.Anything)) .Return(new StreamWrapper(testStream)); // 创建Partial Mock,只拦截Save方法,其他用真实逻辑 var partialEncoder = mockRepo.GeneratePartialMock<PngBitmapEncoder>(); partialEncoder.Expect(e => e.Save(Arg<Stream>.Is.Equal(testStream))); var service = new BitmapService(fileMock, partialEncoder); var testBitmap = BitmapSource.Create(10,10,96,96,PixelFormats.Bgr32,null,new byte[400],40); // Act service.SaveBitmapAsPngImage(testBitmap, "test.png"); // Assert partialEncoder.VerifyAllExpectations(); fileMock.VerifyAllExpectations(); }
关键提醒
- 别直接Mock FileStream:它是密封类,Rhino Mocks没法Mock,用
MemoryStream完全能满足PngBitmapEncoder的流需求。 - 抽象是测试的基础:依赖注入抽象而不是具体类,才是单元测试的正确打开方式。
- 不止断言调用,还要验证结果:检查流的长度大于0,能确保编码器真的写入了数据,而不是空调用。
内容的提问来源于stack exchange,提问作者J Pollack
相关产品推荐
相关产品推荐

