问题
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