【问题标题】:Unit testing with I/O dependencies具有 I/O 依赖关系的单元测试
【发布时间】:2018-04-19 14:07:29
【问题描述】:

我想测试下面的类,但是 I/O 和密封的类依赖关系让它变得非常困难。

public class ImageDrawingCombiner
{
    /// <summary>
    ///     Save image to a specified location in path
    /// </summary>
    /// <param name="path">Location to save the image</param>
    /// <param name="surface">The image as canvas</param>
    public void CombineDrawingsIntoImage(Uri path, Canvas surface)
    {
        Size size = new Size(surface.ActualWidth, surface.ActualHeight);

        // Create a render bitmap and push the surface to it
        RenderTargetBitmap renderBitmap = new RenderTargetBitmap(
            (int)size.Width, (int)size.Height, 96d, 96d, PixelFormats.Pbgra32);
        renderBitmap.Render(surface);

        SaveBitmapAsPngImage(path, renderBitmap);
    }

    // SaveBitmapAsPngImage(path, renderBitmap);
    private void SaveBitmapAsPngImage(Uri path, RenderTargetBitmap renderBitmap)
    {
        // Create a file stream for saving image
        using (FileStream outStream = new FileStream(path.LocalPath, FileMode.OpenOrCreate))
        {
            // Use png encoder for our data
            PngBitmapEncoder encoder = new PngBitmapEncoder();
            // push the rendered bitmap to it
            encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
            // save the data to the stream
            encoder.Save(outStream);
        }
    }
}

稍微重构了 SaveBitmapAsPngImage 方法:

// SaveBitmapAsPngImage(path, renderBitmap, new PngBitmapEncoder());
    public void SaveBitmapAsPngImage(Uri path, BitmapSource renderBitmap, BitmapEncoder pngBitmapEncoder)
    {
        // Create a file stream for saving image
        using (FileStream outStream = new FileStream(path.LocalPath, FileMode.OpenOrCreate))
        {
            // Use png encoder for our data
            // push the rendered bitmap to it
            pngBitmapEncoder.Frames.Add(BitmapFrame.Create(renderBitmap));
            // save the data to the stream
            pngBitmapEncoder.Save(outStream);

}

将其公开以供测试(代码异味?)。它仍在使用 FileStream。有些人会建议用 MemoryStream 和/或工厂模式替换它,但最终它必须保存到某个地方的图像文件中。

即使我用包装器或接口 (SystemInterface) 替换所有基于 I/O 的调用: - 应该在哪里初始化实例?在复合根?这是很多泡沫...... - 如何避免使用 DI 的“最多 3 个构造函数参数”规则? - 这个简单的功能听起来需要做很多工作

测试应确保生成图像文件。

编辑: 尝试运行@Nkosi Moq 测试,但需要修复。替换:

var renderBitmap = new Canvas();

与:

Size renderSize = new Size(100, 50);
var renderBitmap = new RenderTargetBitmap(
    (int)renderSize.Width, (int)renderSize.Height, 96d, 96d, PixelFormats.Pbgra32);

测试结果:

BitmapServiceTest.BitmapService_Should_SaveBitmapAsPngImage 抛出 异常:System.IO.IOException:无法从流中读取。 ---> System.Runtime.InteropServices.COMException:来自 HRESULT 的异常: 0x88982F72 在 System.Windows.Media.Imaging.BitmapEncoder.Save(Stream 流)

似乎编码器对模拟的 Moq 流不满意。 PngBitmapEncoder 依赖项也应该通过方法注入(并在测试中模拟)吗?

【问题讨论】:

  • 该类将图像保存到磁盘上的文件中。似乎这基本上就是它所做的一切。因此,如果您想验证此类的功能,您应该在调用 CombineDrawingsIntoImage 方法后验证磁盘上确实有一个文件。如果你模拟出这个功能,就没有太多要测试的东西了,是吗?
  • 通过调用您的公共方法来测试您的私有方法。如果这变得笨拙,则很有可能您的班级做得太多并且 SaveBitmapAsPngImage 属于另一个班级(可以单独测试)。
  • @mm8 带有文件检查的集成测试是有意义的。在这种情况下,可单元测试的类帮助我验证 conserns 的分离并在设计时牢记 S.O.L.I.D 原则。
  • @adam-g 由于依赖关系,目前无法对公共方法进行单元测试。因此,我想了解什么是完美或最优的设计。
  • 完美是一种非常强烈的赞美。这很棒。阅读他们第一段中括号中的内容,直到它沉入其中。您的班级不需要知道如何保存BitmapAsPngImage。它只需要访问它可以要求这样做的东西(示例中的 IBitmapService)。然后,您可以使用 Moq(或您最喜欢的模拟框架)注入一个假的进行测试。

标签: c# wpf nunit rhino-mocks


【解决方案1】:

这完全是设计问题。尽量避免与实现问题的紧密耦合(类应该依赖于抽象而不是具体化)。

根据您当前的设计考虑以下内容

public interface IBitmapService {
    void SaveBitmapAsPngImage(Uri path, BitmapSource renderBitmap);
}

public interface IFileSystem {
    Stream OpenOrCreateFileStream(string path);
}

public class PhysicalFileSystem : IFileSystem {
    public Stream OpenOrCreateFileStream(string path) {
        return new FileStream(path, FileMode.OpenOrCreate);
    }
}

public class BitmapService : IBitmapService {
    private readonly IFileSystem fileSystem;

    public BitmapService(IFileSystem fileSystem) {
        this.fileSystem = fileSystem;
    }

    // SaveBitmapAsPngImage(path, renderBitmap);
    public void SaveBitmapAsPngImage(Uri path, BitmapSource renderBitmap) {
        // Create a file stream for saving image
        using (var outStream = fileSystem.OpenOrCreateFileStream(path.LocalPath)) {
            // Use png encoder for our data
            PngBitmapEncoder encoder = new PngBitmapEncoder();
            // push the rendered bitmap to it
            encoder.Frames.Add(BitmapFrame.Create(renderBitmap));
            // save the data to the stream
            encoder.Save(outStream);
        }
    }
}

public interface IImageDrawingCombiner {
    void CombineDrawingsIntoImage(Uri path, Canvas surface);
}

public class ImageDrawingCombiner : IImageDrawingCombiner {
    private readonly IBitmapService service;

    public ImageDrawingCombiner(IBitmapService service) {
        this.service = service;
    }

    /// <summary>
    ///  Save image to a specified location in path
    /// </summary>
    /// <param name="path">Location to save the image</param>
    /// <param name="surface">The image as canvas</param>
    public void CombineDrawingsIntoImage(Uri path, Canvas surface) {
        var size = new Size(surface.ActualWidth, surface.ActualHeight);
        // Create a render bitmap and push the surface to it
        var renderBitmap = new RenderTargetBitmap(
            (int)size.Width, (int)size.Height, 96d, 96d, PixelFormats.Pbgra32);
        renderBitmap.Render(surface);
        service.SaveBitmapAsPngImage(path, renderBitmap);
    }
}

FileStream 是一个实现问题,可以在单独进行单元测试时抽象出来。

上面的每个实现都可以单独测试,它们的依赖项可以根据需要被模拟和注入。在生产中,可以使用 DI 容器在组合根中添加依赖项。

如何断言encoder.Save(outStream)被调用?

鉴于您控制流的创建并且System.IO.Stream 是抽象的,您可以轻松地模拟它并验证它是否被写入,因为encode.Save 在执行其功能时必须写入流。

这是一个使用Moq 模拟框架的简单示例,针对上一个示例中的重构代码。

[TestClass]
public class BitmapServiceTest {
    [TestMethod]
    public void BitmapService_Should_SaveBitmapAsPngImage() {
        //Arrange
        var mockedStream = Mock.Of<Stream>(_ => _.CanRead == true && _.CanWrite == true);
        Mock.Get(mockedStream).SetupAllProperties();
        var fileSystemMock = new Mock<IFileSystem>();
        fileSystemMock
            .Setup(_ => _.OpenOrCreateFileStream(It.IsAny<string>()))
            .Returns(mockedStream);

        var sut = new BitmapService(fileSystemMock.Object);
        var renderBitmap = new Canvas();
        var path = new Uri("//A_valid_path");

        //Act
        sut.SaveBitmapAsPngImage(path, renderBitmap);

        //Assert
        Mock.Get(mockedStream).Verify(_ => _.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()));
    }
}

评论员建议使用内存流,我会建议在大多数其他情况下使用,但在这种情况下,流被放置在被测方法中,因为它被包装在 using 语句中。这将使调用成员在被处理后抛出异常。通过直接模拟流,您可以更好地控制断言所调用的内容。

【讨论】:

  • 在这种情况下你会如何安排VS解决方案中的接口和服务(项目/目录)?例如,我有一个 WPF.Main (MVVM)、Logic 和 Logic.Interfaces 项目。 PngBitmapEncoder 不应该吗?如何断言 encoder.Save(outStream) 被调用?非常感谢!
  • 您可以实现一个模拟 IFileSystem,它返回一个内存流而不是一个 FileStream。然后,您可以检查该内存流的内容。或者,您可以将接口提取为 IPngBitmapEncoder 并使用 DI 容器,在测试中注入模拟接口,在生产中注入真实接口。
  • Rhino Stream 存根遇到困难:“encoder.Save(outStream);” System.IO.IOException:无法从流中读取。起订量测试真的有效吗?
  • @JPollack 是的。测试它并通过。我对一些对象使用了一些替换,然后将其调整为适合这个特定场景,作为一个最小的完整可验证示例。
  • 对我来说测试失败了。在我编辑的问题中查看详细信息。
猜你喜欢
  • 1970-01-01
  • 2018-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多