9

I'm trying to use the System.IO.Abstraction project along with System.IO.Abstraction.TestingHelpers to mock a FileStream.

This is the code that's using the file stream that I want to test:

private readonly IFileSystem _fileSystem;

public void ExtractImageAndSaveToDisk(IXLPicture xlPicture, string filePath)
{
    using (MemoryStream ms = new MemoryStream())
    {
        xlPicture.ImageStream.CopyTo(ms);

        using (FileStream fs = (FileStream)_fileSystem.FileStream.Create(filePath, FileMode.Create))
        {
            ms.CopyTo(fs);
            fs.Flush();
            fs.Close(); 
        }
    }
}

And this is how I've set up the testing:

[TestMethod]
public void CheckFileIsSavedToDisk()
{
    // Arrange
    var mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
    {
        { @"c:\Test\Images\", new MockDirectoryData() },
    });

    var xlPicture = GetXLPicture();
    var filePath = @"c:\Test\Images\myimage.jpeg";

    var sut = new CostSheetImageExtractor(mockFileSystem);

    // Act
    sut.ExtractImagesAndSaveToDisk(xlPicture, filePath);
}

Running that I get the exception:

System.InvalidCastException: Unable to cast object of type 'System.IO.Abstractions.TestingHelpers.MockFileStream' to type 'System.IO.FileStream'.

on the using (FileStream fs = ... line.

The first thought was that I need to change the FileStream fs to use an interface that both the real and mock objects share, but as far as I can see there's no IFileStream interface that FileStream and MockFileStream share, so I'm guessing I'm doing this completely wrong? Is there actually a way to test this code with System.IO.Abstraction?

This existing answer seems to suggest this should be possible, I tried doing it that way too but got the same results.

Lukas Körfer
  • 13,515
  • 7
  • 46
  • 62
tomRedox
  • 28,092
  • 24
  • 117
  • 154
  • Why are you writing to an intermediate `MemoryStream`? `CopyTo` is defined on the `Stream` base class so it looks like you can do `using(FileStream fs = ...) { xlPicture.ImageStream.CopyTo(fs); }`. – Lee Oct 22 '18 at 15:56
  • @Lee you're quite right, I stuck that in because the ImageStream is of type `MemoryStream' but I wasn't sure it would be obvious it was a `MemoryStream` without sticking an extra line in to make it clear – tomRedox Oct 22 '18 at 15:59
  • I can't quite work out what the `filesystem` class is -- is that one of your own? – Chris F Carroll Oct 22 '18 at 16:13
  • Hi @ChrisFCarroll, sorry, missed that one when I was cutting out code for the example, I've updated my code now. It's from System.IO.Abstraction: https://github.com/System-IO-Abstractions/System.IO.Abstractions/blob/master/System.IO.Abstractions/FileSystem.cs, which is a library specifically for mocking all parts of System.IO – tomRedox Oct 22 '18 at 16:43
  • Got it. So yes, if you do want to mock the filesystem I'm with Alejandro that `Stream` already is the abstraction you want. – Chris F Carroll Oct 22 '18 at 16:55

2 Answers2

7

Not mocking the filesystem is by far the best option, as Chris F Carroll answer says, but if you want to really do that, your question almost contains the answer.

As you said, there is no such thing as an IFileStream interface, but if you look at the definition of FileStream you'll find that it inherits the abstract class Stream, and that's a common base class for all kinds of streamed data sources. The fake MockFileStream should also inherit from there.

So, try changing your code to use the abstract class instead:

using (Stream fs = (Stream)_fileSystem.FileStream.Create(filePath, FileMode.Create))
{
    ms.CopyTo(fs);
    fs.Flush();
    fs.Close(); 
}

It still retain all the base methods and your code just uses the base ones.

Alejandro
  • 7,290
  • 4
  • 34
  • 59
  • I had the feeling I was being stupid... this confirmed it! Many thanks – tomRedox Oct 22 '18 at 16:39
  • And what if you actually want to use some properties that are defined on FileStream level and do not exist on Stream, like "Name"? – mnj Dec 13 '19 at 15:08
  • 1
    @Loreno Then you need to wrap the `FileStream` in the proposed `IFileStream` interface, the create method return an object implementing that interface (and just wraps the real `FileStream`) that you can mock with any kind of fake implementation. This interface would also contain all the needed non-base properties that the base `Stream` doesn't contains. – Alejandro Dec 13 '19 at 18:36
-2

My first reaction is, you nearly never need to mock the filesystem. You have a Real One, fully working, on your machine, and on your build server, and even in your CI pipeline containers, and it works really really fast too. So why mock it? That appears, on the face of it, to be waste. Mocking is nearly always a compromise with a cost, so not mocking is usually better.

What you might need, if you go this route and test againt the filesystem, is something like System.IO.Directory.GetCurrentDirectory() or System.IO.Path.GetTempPath() to get a place your test suite can call its own, and reliably write, read and (optionally) delete afterwards.

Chris F Carroll
  • 11,146
  • 3
  • 53
  • 61
  • That's a really good point. I think I started doing this out of fear that I would have directory issues when running our tests on VSTS. I've lost a lot of time trying to understand paths in VSTS in the past! – tomRedox Oct 22 '18 at 16:11
  • 18
    Because what happens when the disk is full and you can't write the file? Your tests fail, that's what happens. The file system is an integration point, and integration points are typically mocked. – Kenneth K. Oct 22 '18 at 16:37
  • 1
    If your filesystem is full your deployment and test run are going to fail anyway. – Chris F Carroll Oct 22 '18 at 16:44
  • Yes I typically mock integration points because that is faster/cheaper/more reliable than pulling the real thing into the test. The FileSystem is obvious exception: mocking it is not cheaper than using the real one. – Chris F Carroll Oct 22 '18 at 16:45
  • Could permissions end up being an issue if using the real filesystem - especially on a build server? – tomRedox Oct 22 '18 at 16:46
  • Yes. But. A build process must have write permissions to ... build. So for unit tests I have found `System.IO.Directory.GetCurrentDirectory()` reliable. I've only needed to use `System.IO.Path.GetTempPath()` for a process that ran under IIS – Chris F Carroll Oct 22 '18 at 16:51
  • 3
    "If your filesystem is full your deployment and test run are going to fail anyway." You're neglecting accessing network locations. – Kenneth K. Oct 22 '18 at 17:01
  • That's really useful to know @ChrisFCarroll, thanks for taking the time to explain it – tomRedox Oct 22 '18 at 17:02
  • Unless you use unique path to file, running on CI could cause multiple instances accessing same file. – SebS Feb 24 '20 at 03:01
  • Agreed, an instance of any spawnable process has to know how to get it's own private file. I can't name an OS or VM that doesn't offer an API to meet that requirement – Chris F Carroll Feb 25 '20 at 12:38
  • 5
    "You have a Real One, fully working, [...] and it works really really fast too. So why mock it?" - The same is true for network connections and databases, so why should we mock them? We mock them because our tests should only depend on our (production) code and our test code. This rule gets violated as soon as we access systems we cannot fully control. It is perfectly fine to integrate with such external systems to test your code, but those tests will be integration tests, not unit tests. – Lukas Körfer Dec 14 '20 at 19:59
  • NO! The same is *not at all* true for network connnections, databases, services. They may not be under your control, they may be fragile, they may cost weeks to recreate in a CI pipeline. _The FileSystem is different in One, Very Simple way_: Every machine in your Dev and Build and CI pipeline has one, and it works. Why blind yourself to this? – Chris F Carroll Dec 15 '20 at 20:23
  • 6
    There're plenty scenarios where mocking a file system is useful. For example, will the program handle UnauthorizedAccessException properly? Will the FileStream be properly disposed in case of an error? Is the bug that happens when the program seeks 5GB into the file still there? Will a file be overwritten only if certain conditions are met? Will the program avoid reading certain files? These scenarios can be difficult to test without mocking. The file system is an integration point and often it's important to test if the program interacts with it as intended. Mocking is a useful tool to do so. – oscfri Mar 08 '21 at 21:17
  • @oscfri I like all your examples of real world issues. If the component I am writing and testing is not the component responsible for handling errors in external components though, then neither my component nor its tests should have code for the error scenarios. Agree mocking is a useful tool. I disagree that using a Mock is in any way preferable to using a Real Thing — except when it's cheaper to use than using a Real Thing? – Chris F Carroll Mar 09 '21 at 19:51
  • 1
    @ChrisFCarroll If you have many tests running in parallel are you certain you won't have a conflict? Is it faster to create a file, open the file, lock the file, spawn another thread, try to open the file and write to it and test how it handles the exception? Is it faster to get OS permissions on a file and then attempt to violate them? Does your CI system run on the same operating system? Is the storage it uses as a VM/container/etc the same as what production will use? – Cryolithic Jan 20 '22 at 09:29