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

为密封类PngBitmapEncoder伪造流:单元测试I/O依赖后续问题

嘿,我来帮你搞定这个测试问题!这确实是《带有I/O依赖的单元测试》的典型后续场景——要验证PngBitmapEncoder的Save调用,同时避开真实的文件系统依赖。下面是用Rhino Mocks实现的完整方案:

核心思路

咱们要解决两个核心问题:

  1. 让PngBitmapEncoder接受可控的模拟流(不用真实FileStream)
  2. 明确断言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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 08:54:01