C# 具有I/O依赖项的单元测试

C# 具有I/O依赖项的单元测试,c#,wpf,nunit,rhino-mocks,C#,Wpf,Nunit,Rhino Mocks,我想测试下面的类,但是I/O和密封的类依赖关系使它变得非常困难 public class ImageDrawingCombiner { /// <summary> /// Save image to a specified location in path /// </summary> /// <param name="path">Location to save the image</param>

我想测试下面的类,但是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);
        }
    }
}
公开了可测试性(代码气味?)。它仍在使用FileStream。有人建议用MemoryStream和/或Factory模式替换它,但最终它必须保存到某个位置的图像文件中

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

测试应确保生成图像文件

编辑: 试图运行@Nkosi Moq测试,但需要修复。取代:

var renderBitmap = new Canvas();
与:

测试结果:

BitmapServiceTest.BitmapService_应保存BitMapAspngImage 异常:System.IO.IOException:无法从流中读取。--> System.Runtime.InteropServices.ComeException:来自HRESULT的异常: 0x88982F72 位于System.Windows.Media.Imaging.BitmapEncoder.Save(流)


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

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

根据您当前的设计考虑以下几点

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

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

该类将图像保存到磁盘上的文件中。看起来它基本上就是这么做的。因此,如果要验证此类的功能,应在调用CombineDrawingsIntoImage方法后验证磁盘上是否存在文件。如果您模拟了这个功能,那么就没有什么需要测试的了,是吗?通过调用公共方法来测试您的私有方法。如果这变得不方便,很可能是您的类做得太多,而saveBitMapAspNginImage属于另一个类(可以单独测试)。@mm8文件检查集成测试有意义。在这种情况下,单元可测试类帮助我验证conserns的分离,并将其设计为S.O.L.I.D原则。@adam-g由于依赖关系,目前无法对公共方法进行单元测试。因此,我想知道什么是完美或最优的设计。完美是一种非常强烈的赞美。非常好。阅读他们第一段括号中的一位,直到它完全理解。您的类不需要知道如何保存BitMapAspNginImage。它只需要访问它可以请求的东西(示例中的IBitmapService)。然后,您可以使用Moq(或您最喜欢的模拟框架)注入一个假的框架进行测试。在这种情况下,您将如何在VS解决方案中安排接口和服务(项目/目录)?例如,我有一个WPF.Main(MVVM)、Logic和Logic.Interfaces项目。PngBitmapEncoder不应该吗?如何断言调用了encoder.Save(扩展流)?非常感谢!您可以实现一个模拟IFileSystem,它返回的是memorystream而不是文件流。然后可以检查该内存流的内容。或者,您可以将接口提取为IPngBitmapEncoder,并使用DI容器,在测试中注入模拟接口,在生产中注入真实接口。Rhino流存根有困难:“encoder.Save(outStream);“System.IO.IOException:无法从流中读取。Moq测试真的有效吗?@JPollack是的。测试并通过。我对一些对象使用了一些替换,然后将其调整为适合此特定场景,作为一个最小的完整可验证示例。对我来说,测试失败了。请参阅我编辑的问题中的详细信息。
Size renderSize = new Size(100, 50);
var renderBitmap = new RenderTargetBitmap(
    (int)renderSize.Width, (int)renderSize.Height, 96d, 96d, PixelFormats.Pbgra32);
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);
    }
}
[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>()));
    }
}