具有 I/O 个依赖项的单元测试
Unit testing with I/O dependencies
我想测试以下 class 但 I/O 和密封的 class 依赖项使它变得非常困难。
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);
}
使其 public 可测试(代码味道?)。它仍在使用 FileStream。有人会建议用 MemoryStream and/or 工厂模式替换它,但最后它必须保存到某个地方的图像文件中。
即使我用包装器或接口 (SystemInterface) 替换所有基于 I/O 的调用:
- 应该在哪里初始化实例?在复合根?泡沫太多了...
- 如何使用 DI 避免 "up to 3 constructor parameter" 规则?
- 对于这个简单的功能来说,这一切听起来工作量很大
测试应确保生成图像文件。
编辑:
尝试 运行 @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 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 依赖项是否也应该通过方法注入(并在测试中模拟)?
这完全是设计问题。尽量避免与实现问题紧密耦合(类 应该依赖于抽象而不是具体化)。
根据您当前的设计考虑以下内容
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 容器在组合根中添加依赖项。
How to assert that encoder.Save(outStream)
is called?
鉴于您控制流的创建并且 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
语句中。这将使流中的调用成员在处理后抛出异常。通过完全模拟流,您可以更好地控制断言所调用的内容。
我想测试以下 class 但 I/O 和密封的 class 依赖项使它变得非常困难。
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);
}
使其 public 可测试(代码味道?)。它仍在使用 FileStream。有人会建议用 MemoryStream and/or 工厂模式替换它,但最后它必须保存到某个地方的图像文件中。
即使我用包装器或接口 (SystemInterface) 替换所有基于 I/O 的调用: - 应该在哪里初始化实例?在复合根?泡沫太多了... - 如何使用 DI 避免 "up to 3 constructor parameter" 规则? - 对于这个简单的功能来说,这一切听起来工作量很大
测试应确保生成图像文件。
编辑: 尝试 运行 @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 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 依赖项是否也应该通过方法注入(并在测试中模拟)?
这完全是设计问题。尽量避免与实现问题紧密耦合(类 应该依赖于抽象而不是具体化)。
根据您当前的设计考虑以下内容
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 容器在组合根中添加依赖项。
How to assert that
encoder.Save(outStream)
is called?
鉴于您控制流的创建并且 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
语句中。这将使流中的调用成员在处理后抛出异常。通过完全模拟流,您可以更好地控制断言所调用的内容。