Many applications require access to the file system to create, modify or delete files and folders. But how do you make sure that such application behaves correctly? You do it with tests of course but there is a catch: In general it is not a good idea to have tests that are performing Input/Output operations like accessing files and databases. When you need to test I/O operations mock objects are your friend. And before I go into more details let me point out some of the benefits of mocking.

  • Usually faster than performing I/O operations
  • You do not have have to deal with security issues and permissions
  • You have more control – easily test intricate scenarios
  • Test setup is far more easier to do 

Let’s start with a very simple class called FileManager which can perform a single operation called Rename. I want to make sure that Rename will correctly rename a specified file. For the sake of simplicity Rename will always rename the specified file to “ranamed.file”. Here is the code of FileManager:

public class FileManager

{

    private IFileSystem fileSystem;

 

    public FileManager(IFileSystem fileSystem)

    {

        if (fileSystem == null)

            throw new ArgumentNullException("fileSystem");

 

        this.fileSystem = fileSystem;

    }

 

    public void Rename(string filePath)

    {

        if (this.FileDoesNotExist(filePath))

            throw new FileNotFoundException("File was not found", filePath);

 

        var renamedFilePath = "renamed.file";

 

        if (this.FileExists(renamedFilePath))

            throw new IOException("New filename already exists: " + renamedFilePath);

 

        this.fileSystem.MoveFile(filePath, renamedFilePath);

    }

 

    private bool FileDoesNotExist(string filePath)

    {

        return !this.FileExists(filePath);

    }

 

    private bool FileExists(string filePath)

    {

        return this.fileSystem.FileExists(filePath);

    }

}

 

By taking a quick look at Rename you will notice that it has the following behavior which has to be tested:

  1. FileNotFoundException should be thrown when the specified file does not exist
  2. IOException should be thrown when the renamed file exists
  3. If no exceptions are encountered MoveFile will perform the actual rename operation

So how do we go about testing this behavior? If you take a look at the constructor of FileManager you will notice that it accepts a single argument of type IFileSystem. This interface defines all operations that can be perform on the file system. All I/O operations that FileManager performs are executed through this interface.

public interface IFileSystem

{

    bool FileExists(string path);

 

    void MoveFile(string filePath, string newFilePath);

}

 

As we will see, IFileSystem allows me to create a fake implementation of the file system functionality (mock) which will be the cornerstone of my I/O tests.

I will be using MSTest to drive my tests but you can use a testing framework of your choice. Although you are free to create mocks by hand you will be better off using a mocking framework. I have chosen the new, but yet powerful, kid on the block - JustMock. Armed with JustMock you will be able to easily mock interfaces and classes. In this particular case I will use JustMock to mock the IFileSystem interface.

Let’s write some tests.

I use the Initialize method to create a mock implementation of IFileSystem and use it to initialize the FileManager that will be tested.

[TestClass]

public class RenameTests

{

    private IFileSystem fileSystem;

    private FileManager fileManager;

 

    [TestInitialize]

    public void Initialize()

    {

        this.fileSystem = Mock.Create<IFileSystem>();

        this.fileManager = new FileManager(this.fileSystem);

    }

 

    // tests ...

}

 

Assert that exception is thrown when non-existent file is passed to Rename

[TestMethod]

[ExpectedException(typeof(FileNotFoundException))]

public void Rename_Throws_WhenTheSpecifiedFileDoesNotExist()

{

    Mock.Arrange(() => this.fileSystem.FileExists("file.to.rename")).Returns(false);

 

    this.fileManager.Rename("file.to.rename");

}

 

The magic here happens on the first line of the test where we call Mock.Arrange. This call makes sure that when FileExists is called with “file.to.rename” argument it will return false. After that we simply call Rename and if FileNotFoundException is raised than we have a correct behavior.

 Assert that exception is thrown when new filename already exists 

[TestMethod]

[ExpectedException(typeof(IOException))]

public void Rename_Throws_WhenTheTheNewFileNameExists()

{

    Mock.Arrange(() => this.fileSystem.FileExists("file.to.rename")).Returns(true);

    Mock.Arrange(() => this.fileSystem.FileExists("renamed.file")).Returns(true);

 

    this.fileManager.Rename("file.to.rename");

}

 

As you can see this test is very similar to the first one. The difference is that we first have to ensure that FileExists(“file.to.rename”) returns true – this makes sure that the fist condition of the Rename method is met (input file exists). Since we know that Rename will always rename the input file to “renamed.file” we instruct the file system mock to return true for FileExists(“renamed.file”). This way we simulate the case where the file we want to rename exists but the new filename already exists on disk. The last step is to actually call Rename.

 Assert that file is actually renamed

[TestMethod]

public void Rename_RenamesTheInputFile_WhenInputExistsAndNewFileNameDoesNotExist()

{

    Mock.Arrange(() => this.fileSystem.FileExists("file.to.rename")).Returns(true);

    Mock.Arrange(() => this.fileSystem.FileExists("renamed.file")).Returns(false);

 

    this.fileManager.Rename("file.to.rename");

 

    Mock.Assert(() => this.fileSystem.MoveFile("file.to.rename", "renamed.file"), Occurs.Once());

}

Finally we have to make sure that file is actually renamed when no exception are encountered.

This test is a very good demonstration of the Arrange Act Assert pattern of writing tests. First we setup the preconditions of the test – in this case we ensure that FileExists(“file.to.rename”) returns true and FileExists(“renamed.file”) returns false. After that we call the method that we are testing. Finally we assert that our expectations are correct. In this particular case we assert that Rename calls MoveFile where the first argument is the old filename (“file.to.rename”) and the second one is the new filename (“renamed.file”).

What do you think about those tests?

I believe that they are easy to read and modify. Moreover they are all short which is always a good property of a test.

There is one more thing. How do we use FileRenamer in a real application? Well, we first create an implementation of IFileSystem that will actually invoke the file system.

public sealed class DefaultFileSystem : IFileSystem

{

    public bool FileExists(string path)

    {

        return File.Exists(path);

    }

 

    public void MoveFile(string filePath, string newFilePath)

    {

        File.Move(filePath, newFilePath);

    }

}

 

And finally we create a FileManager with DefaultFileSystem.

class Program

{

    static void Main(string[] args)

    {

        FileManager manager = new FileManager(new DefaultFileSystem());

 

        manager.Rename("file.to.rename");

    }

}

 

You can download the source code from here (VS2010 & VS2008)

Cheers.


Comments

Comments are disabled in preview mode.