Unit testing with I/O dependencies

喜欢而已 提交于 2019-12-24 09:49:59

问题


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);
        }
    }
}

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

}

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.

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.

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

var renderBitmap = new Canvas();

with:

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

Test result:

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)

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


回答1:


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 is an implementation concern that can be abstracted out when unit testing in isolation.

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.

How to assert that encoder.Save(outStream) is called?

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.

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>()));
    }
}

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.



来源:https://stackoverflow.com/questions/49923188/unit-testing-with-i-o-dependencies

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!