Mocking System.IO filesystem in unit tests in ASP.NET Core
Image from Pixabay

Mocking System.IO filesystem in unit tests in ASP.NET Core

Testable filesystem operations in ASP.NET Core and C#

Working with file system operations like creating or deleting files and directories if quite often part of applications flow. Both .NET and .NET Core come with great out of the box classes and methods to achieve this.

These classes and methods are part of System.IO name space, but unfortunately both .NET and .NET Core implementations are the same and they use static classes and method to manipulate files and directories on the host file system.

Static System Io

Now when it comes to unit testing, it is not possible to mock these classes and methods for the simple reason they are static. The solution would be to write interfaces and then just add those interface implementations that just wrap System.IO classes and methods.

This seems like the way to do it, but System.IO filesystem methods and classes grew over time as .NET framework was evolving, so writing implementations to cover all methods would take some time to develop and of course to test. A nice alternative which I use is System.IO.Abstractions NuGet package which covers all System.IO file and directory methods and classes that are commonly used in every day coding and it is pretty easy to use it and write tests around it.

I created a small sample AS.NET Core application that relies on System.IO classes and methods to access files and directories on the hosting filesystem. For the simplicity I skipped creating AppServices layer and will just put the file/folder operations in the controller.

using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;

namespace SampleApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class StorageController : ControllerBase
    {
        public IActionResult Get()
        {
            String filePath = @"c:\temp\file.txt";
            String folderPath = Path.GetDirectoryName(filePath);

            if(!Directory.Exists(folderPath)){
                Directory.CreateDirectory(folderPath);
            }
            else if(System.IO.File.Exists(filePath))
            {
                System.IO.File.Delete(filePath);
            }

            System.IO.File.WriteAllText(filePath, "Hello world");
            var content = System.IO.File.ReadAllText(filePath);

            return Ok(content);
        }
    }
}
    

Nothing special here, just few basic, most commonly used file and folder operations that you probably use in your every day to day coding and you do not pay much attention to it. However, when you need to write unit test for this controller method you will really struggle to do it.

Now let's see how would this look like with System.IO.Abstractions in place.

First things first. Before you dig into coding and refactoring this method, you need to add a NuGet package reference to your project and setup the IoC container in Startup.cs. 

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
    <PackageReference Include="System.IO.Abstractions" Version="7.1.3" />
  </ItemGroup>

</Project>
    

Package reference is in place, so we can setup the IoC container of our ASP.NET Core application

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IFileSystem, FileSystem>();
            services.AddControllers();
        }
    

Let's refactor controller from above to use System.IO.Abstractions and see how easy it will be to write unit test for it after refactoring.

using System;
using System.IO.Abstractions;
using Microsoft.AspNetCore.Mvc;

namespace SampleApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class StorageController : ControllerBase
    {
        readonly IFileSystem _fileSystem;

        public StorageController(IFileSystem fileSystem)
        {
            _fileSystem = fileSystem;
        }

        public IActionResult Get()
        {
            String filePath = @"c:\temp\file.txt";
            String folderPath = _fileSystem.Path.GetDirectoryName(filePath);

            if(!_fileSystem.Directory.Exists(folderPath)){
                _fileSystem.Directory.CreateDirectory(folderPath);
            }
            else if(_fileSystem.File.Exists(filePath))
            {
                _fileSystem.File.Delete(filePath);
            }

            _fileSystem.File.WriteAllText(filePath, "Hello world");
            var content = _fileSystem.File.ReadAllText(filePath);

            return Ok(content);
        }
    }
}
    

If you compare this controller to the previous one you won't notice any big difference except we are now completely relying on injected IFileSystem interface implementation. Method signatures for files and folder are identical to the static ones from System.IO namespace which reference is now completely removed from using the the top of the class file.

Let's see now how to write some simple unit tests for this controller with Moq package and Xunit.

using Moq;
using SampleApi.Controllers;
using System;
using System.IO.Abstractions;
using Xunit;
using AutoFixture;
using Microsoft.AspNetCore.Mvc;

namespace SampleApi.Tests
{
    public class StorageControllerTests
    {
        readonly Mock<IFileSystem> _fileSystem;
        readonly Fixture _fixture;
        public StorageControllerTests()
        {
            _fixture = new Fixture();
            _fileSystem = new Mock<IFileSystem>();
            _fileSystem.Setup(f => f.Directory.CreateDirectory(It.IsAny<String>())).Verifiable();
            _fileSystem.Setup(f => f.File.Delete(It.IsAny<String>())).Verifiable();
            _fileSystem.Setup(f => f.File.WriteAllText(It.IsAny<String>(),It.IsAny<String>())).Verifiable();
            _fileSystem.Setup(f => f.File.ReadAllText(It.IsAny<String>())).Returns(_fixture.Create<String>());
        }

        [Fact]
        public void Get_FolderDoesNotExists_Returns_OK()
        {
            _fileSystem.Setup(f => f.Directory.Exists(It.IsAny<String>())).Returns(false);
            var controller = new StorageController(_fileSystem.Object);
            var result = controller.Get();

            Assert.NotNull(result);
            Assert.IsAssignableFrom<OkObjectResult>(result);

            _fileSystem.Verify(f => f.Directory.CreateDirectory(It.IsAny<String>()), Times.Once);
            _fileSystem.Verify(f => f.File.Delete(It.IsAny<String>()), Times.Never);
            _fileSystem.Verify(f => f.File.WriteAllText(It.IsAny<String>(),It.IsAny<String>()), Times.Once);
            _fileSystem.Verify(f => f.File.ReadAllText(It.IsAny<String>()), Times.Once);
        }

        [Fact]
        public void Get_FolderExists_FileDoesNotExists_Returns_OK()
        {
            _fileSystem.Setup(f => f.Directory.Exists(It.IsAny<String>())).Returns(true);
            _fileSystem.Setup(f => f.File.Exists(It.IsAny<String>())).Returns(false);
            var controller = new StorageController(_fileSystem.Object);
            var result = controller.Get();

            Assert.NotNull(result);
            Assert.IsAssignableFrom<OkObjectResult>(result);

            _fileSystem.Verify(f => f.Directory.CreateDirectory(It.IsAny<String>()), Times.Once);
            _fileSystem.Verify(f => f.File.Delete(It.IsAny<String>()), Times.Never);
            _fileSystem.Verify(f => f.File.WriteAllText(It.IsAny<String>(), It.IsAny<String>()), Times.Once);
            _fileSystem.Verify(f => f.File.ReadAllText(It.IsAny<String>()), Times.Once);
        }

        [Fact]
        public void Get_FolderExists_FileExists_Returns_OK()
        {
            _fileSystem.Setup(f => f.Directory.Exists(It.IsAny<String>())).Returns(true);
            _fileSystem.Setup(f => f.File.Exists(It.IsAny<String>())).Returns(true);
            var controller = new StorageController(_fileSystem.Object);
            var result = controller.Get();

            Assert.NotNull(result);
            Assert.IsAssignableFrom<OkObjectResult>(result);

            _fileSystem.Verify(f => f.Directory.CreateDirectory(It.IsAny<String>()), Times.Once);
            _fileSystem.Verify(f => f.File.Delete(It.IsAny<String>()), Times.Once);
            _fileSystem.Verify(f => f.File.WriteAllText(It.IsAny<String>(), It.IsAny<String>()), Times.Once);
            _fileSystem.Verify(f => f.File.ReadAllText(It.IsAny<String>()), Times.Once);
        }
    }
}

    

As you can see, writting tests for method that work with files and folder is quite easy this way.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article