带有 I/O 依赖项的单元测试 [英] Unit testing with I/O dependencies

查看:51
本文介绍了带有 I/O 依赖项的单元测试的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想测试以下类,但 I/O 和密封类依赖项使它变得非常困难.

I would like to test the following class but I/O and sealed class dependencies are making it quite hard.

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 方法:

Refactored the SaveBitmapAsPngImage method a bit:

// 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 和/或工厂模式替换它,但最终它必须保存到某个位置的图像文件中.

Made it public to be testable (code smell?). It is still using FileStream. Some would suggest to replace it with MemoryStream and/or Factory pattern but in the end it has be to saved to the image file somewhere.

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

Even if I replace all the I/O based calls with wrappers or interfaces (SystemInterface): - Where should the instances be initialised? At the composite root? That is a lot to bubble up... - How would I avoid the "up to 3 constructor parameter" rule with DI? - It all sounds a lot of work for this simple function

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

The test(s) should make sure the image file is produces.

尝试运行 @Nkosi Moq 测试,但需要修复.已替换:

Tried to run the @Nkosi Moq test but it needed a repair. Replaced:

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 stream)

BitmapServiceTest.BitmapService_Should_SaveBitmapAsPngImage threw exception: System.IO.IOException: Cannot read from the stream. ---> System.Runtime.InteropServices.COMException: Exception from HRESULT: 0x88982F72 at System.Windows.Media.Imaging.BitmapEncoder.Save(Stream stream)

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

Seems like the encoder is not happy with the mocked Moq stream. Should the PngBitmapEncoder dependency also by method injected (and mocked in tests)?

推荐答案

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

This is all a matter of design. Try to avoid tight coupling to implementation concerns (classes should depend on abstractions and not on concretions).

根据您当前的设计考虑以下事项

Consider the following based on your current design

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 是一个实现问题,可以在单独进行单元测试时抽象出来.

FileStream is an implementation concern that can be abstracted out when unit testing in isolation.

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

Every implementation above can be tested on its own in isolation with their dependencies capable of being mocked and injected as needed. In production, dependencies can be be added in the composition root with a DI container.

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

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

Given that you control the creation of the stream and that System.IO.Stream is abstract you can easily mock it and verify that it was written to as the encode.Save would have to write to the stream while performing its functions.

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

Here is a simple example using Moq mocking framework targeting the refactored code in the previous example.

[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 语句中.这将使流上的调用成员在被处理后抛出异常.通过直接模拟流,您可以更好地控制断言所调用的内容.

A commentor suggested using a memory stream, which I would suggest in most other scenarios, but in this case the stream is being disposed within the method under test as it is wrapped in a using statement. This would make calling members on the stream after being disposed to throw exceptions. By mocking the stream outright you have more control of asserting what was called.

这篇关于带有 I/O 依赖项的单元测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆