C# – Unit testing code that does File IO

If your code does File IO, such as reading text from a file, then it’s dependent on the file system. This is an external dependency. In order to make the unit tests fast and reliable, you can mock out the external dependencies.

To mock out the file system dependency, you can wrap the File IO method calls, extract an interface for this wrapper, and dependency inject the wrapper. In this article, I’ll show how to do this technique to be able to unit test code that does File IO.

First, the code under test

Here’s an example of code that does File IO. It’s calling static methods in System.IO.File to interact with the file system. It’s checking if a file exists, reading the text, and elsewhere it’s saving text to a file.

using System.IO; public class WordCountService { public Dictionary<string, int> GetWordCounts(string path) { if (!File.Exists(path)) { throw new FileNotFoundException(path); } var wordCounts = File.ReadAllText(path) .Split() .GroupBy(s => s).ToDictionary(word => word.Key, word => word.Count()); wordCounts.Remove(""); //better than the verbose Split() overload to ignore empties return wordCounts; } public void SaveWordCounts(Dictionary<string, int> wordCounts, string path) { StringBuilder sb = new StringBuilder(); foreach(var wordCount in wordCounts) { sb.AppendLine($"{wordCount.Key}={wordCount.Value}"); } File.WriteAllText(path, sb.ToString()); } }
Code language: C# (cs)

If you tried to test this without mocking out the file system, you’d need to deal with actual files in the tests, which would make things more complicated. Furthermore, if you’re using real files, you could run into speed and reliability problems.

1 – Wrap the File IO methods

The first step to making this code unit testable is to wrap the File IO methods in a wrapper class, and extract out an interface for that wrapper.

Here is the wrapper class. It wraps the three File IO methods that the code is calling.

using System.IO; public class FileIOWrapper : IFileIOWrapper { public bool Exists(string path) { return File.Exists(path); } public string ReadAllText(string path) { return File.ReadAllText(path); } public void WriteAllText(string path, string text) { File.WriteAllText(path, text); } }
Code language: C# (cs)

Here’s the interface for the wrapper. This is necessary to be able to mock out the wrapper class in the unit tests.

public interface IFileIOWrapper { bool Exists(string path); string ReadAllText(string path); void WriteAllText(string path, string text); }
Code language: C# (cs)


2 – Dependency inject the wrapper interface

In order to be able to pass in the mock in the unit tests, you have to add the wrapper interface as a constructor parameter. This is referred to as dependency injection (which has other benefits besides making the code unit testable).

public class WordCountService { private readonly IFileIOWrapper FileIOWrapper; public WordCountService(IFileIOWrapper fileIOWrapper) { FileIOWrapper = fileIOWrapper; } //rest of class }
Code language: C# (cs)

Now update the code to use the FileIOWrapper methods instead of the System.IO.File methods:

public Dictionary<string, int> GetWordCounts(string path) { if (!FileIOWrapper.Exists(path)) { throw new FileNotFoundException(path); } var wordCounts = FileIOWrapper.ReadAllText(path) .Split() .GroupBy(s => s).ToDictionary(word => word.Key, word => word.Count()); wordCounts.Remove(""); //better than the verbose Split() overload to ignore empties return wordCounts; } public void SaveWordCounts(Dictionary<string, int> wordCounts, string path) { StringBuilder sb = new StringBuilder(); foreach(var wordCount in wordCounts) { sb.AppendLine($"{wordCount.Key}={wordCount.Value}"); } FileIOWrapper.WriteAllText(path, sb.ToString()); }
Code language: C# (cs)


3 – Write a unit test and mock out the wrapper

In the unit test, create the mock IFileIOWrapper and configure it as desired depending on what you’re testing. Pass the mock as a constructor parameter to the code under test (WordCountService in this case).

In this example, it’s verifying that the code throws an exception when the file doesn’t exist:

using Moq; [TestMethod()] public void GetWordCountsTest_WhenFileDoesntExist_Throws() { //arrange var path = @"C:\book.txt"; var mockFileIO = new Mock<IFileIOWrapper>(); mockFileIO.Setup(t => t.Exists(path)).Returns(false); var wordCountService = new WordCountService(mockFileIO.Object); //act and assert Assert.ThrowsException<FileNotFoundException>(() => wordCountService.GetWordCounts(path)); }
Code language: C# (cs)

Here’s another example. This is configuring the IFileIOWrapper mock to return a small amount of text, and then verifying that GetWordCounts() correctly parses the text into a word count map.

[TestMethod()] public void GetWordCountsTest_WhenFileExists_ReturnsWordCountsFromFileText() { //arrange var sb = new StringBuilder(); sb.AppendLine("hello world"); sb.AppendLine("hello code"); var expectedCounts = new Dictionary<string, int>() { ["hello"] = 2, ["world"] = 1, ["code"] = 1 }; var path = @"C:\book.txt"; var mockFileIO = new Mock<IFileIOWrapper>(); mockFileIO.Setup(t => t.Exists(path)).Returns(true); mockFileIO.Setup(t => t.ReadAllText(path)).Returns(sb.ToString()); var wordCountService = new WordCountService(mockFileIO.Object); //act var wordCounts = wordCountService.GetWordCounts(path); //assert CollectionAssert.AreEquivalent(expectedCounts, wordCounts); }
Code language: C# (cs)

Leave a Comment