27 Nov 2021
My advice is simple: use the System.IO.Abstractions
package from TestableIO for your
C# projects. I will never use the raw System.IO
package again.
Why? Unit testing of course!
The System.IO
namespace has some incredibly useful classes and
methods. If your code uses the static methods from the File
or
Directory
classes, you will have a time of it, unit testing
with those in there. I have never found a suitable, meaning easy, way to
write unit test classes without accessing the file system when using
those methods.
This code represent something fairly common, from my past, when processing data from files. Don't get hung up on the code itself but that look at the pattern for reading from a file.
public class FileSample
{
public bool ProcessText(string fileName)
{
var exists = File.Exists(fileName);
if (exists)
{
var text = File.ReadAllText(fileName);
// do something with `text`
return true;
}
Console.WriteLine($"{fileName} not found!");
return false;
}
}
This style of code made it rather difficult to write unit tests when it
contained references to the static methods in the File
class.
The traditional workaround is to create an interface that can be mocked when
used in the unit testing code.
public interface IFileWrapper
{
bool Exists(string fileName);
string ReadAllText(string fileName);
// same for all other methods used in your code
}
The default implementation would be coded that forwarded the calls to actual static methods. There is not a lot of code, but it is code that needs to be written and maintained. As with all code, there is a cost.
public class DefaultFileWrapper : IFileWrapper
{
public bool Exists(string fileName) => File.Exists(fileName);
public string ReadAllText(string fileName) => File.ReadAllText(fileName);
}
It is the same for every project that uses the File
and
Directory
classes. I am sure there are other classes that
should get the same treatment, but these two (2) are the biggest culprits
in my coding life. While I enjoy coding this particlar pattern does get
boring: write code, extract wrapper, write default implementation, and
repeat (for all things required). After you do this on a few projects you
begin to contemplate the cost of creating a library to handle this
nonsense for you.
public class WrapperNonsense
{
private IFileWrapper _wrapper;
public WrapperNonsense() : this(new DefaultFileWrapper())
{
}
public WrapperNonsense(IFileWrapper wrapper)
{
_wrapper = wrapper ?? throw new ArgumentNullException(nameof(wrapper));
}
public bool ProcessText(string fileName)
{
var exists = _wrapper.Exists(fileName);
if (exists)
{
var text = _wrapper.ReadAllText(fileName);
// do something with `text`
return true;
}
Console.WriteLine($"{fileName} not found!");
return false;
}
}
The good news is that the work has already been done! The team at
TestableIO has already created
an abstractions package for making the System.IO
namespace easy
to unit test with: System.IO.Abstractions.
The only trick left is injecting their IFileSystem
interface
into your class and calling the proper methods. You were doing that already
for your implementation so it is not a big deal (or you weren't writing unit
tests and that is a bigger deal). The TestableIO
code provides a solution that performs exactly the same operations as your
code.
This code should look familiar. Access to the abstraction is managed through
the File
and Directory
properties. There are others
in there, including some help with mock objects.
public class AbstractionSample
{
private IFileSystem _fs;
public AbstractionSample() : this(new FileSystem())
{
}
public AbstractionSample(IFileSystem fs)
{
_fs = fs ?? throw new ArgumentNullException(nameof(fs));
}
public bool ProcessText(string fileName)
{
var exists = _fs.File.Exists(fileName);
if (exists)
{
var text = _fs.File.ReadAllText(fileName);
// do something with `text`
return true;
}
Console.WriteLine($"{fileName} not found!");
return false;
}
}
This library allows you to write less code, get the same effect, and have the code that you do write be more testable. It was what I was doing anyway! It is a win, win!
Using this library for greenfield development is a no-brainer. Existing code will be modified to utilize this library as changes are required.
< back