Pages

Wednesday, 18 August 2010

Testing with the file system

I've come across the following solutions to writing tests which require access to the file system:
  • Write Integration tests, not unit tests. For this to work you need a simple way of creating a folder where you can dump stuff without worrying about other tests interfering. I have a simple TestFolder (see below) class which can create a unique per test method folder to use.
  • Write a mockable System.IO.File. That is create a IFile.cs. I find using this often ends up with tests that simply prove you can write mocking statements, but do use it when the IO usage is small.
  • Examine you layer of abstraction, and extract the file IO from the class. The create a interface for this. The remainder use integration tests (but this will be very small). This differs from above in that instead of doing file.Read you write the intent, say ioThingie.loadSettings()

I end up using all the methods above, depending on what I'm writing. But most of the time I end up thinking abstraction is wrong when I write unit tests that hit the IO.
    TestFolder Class:
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    namespace IHazTehCodez.Common.Test.IO
    {
        ///
        /// Generates a folder to use for intergration tests. The returned folder is unique pre test method.
        ///
        public class TestFolder
        {

            ///
            /// By default test folders are created in %TEMP%\tests. This ctor allows that to be changed, for
            /// example to run the same test concurently with each instance having a different root could be used .
            ///
            ///
    The root folder.
            public TestFolder(string root)
            {
                if (root.EndsWith("\\"))
                {
                    Root = root;
                } else
                {
                    Root = root + "\\";
                }
            }

            ///
            /// Generates test folders using the default %TEMP%\tests.
            ///
            public TestFolder()
                : this(Path.GetTempPath() + "\\tests\\")
            {
            }

            ///
            /// The root folder, off which all other test folders are generated.
            ///
            public string Root { get; private set; }


            internal string Folder()
            {
                StackFrame[] stack = (new StackTrace()).GetFrames();
            
                ValidateStackFrames(stack);

                foreach (var stackFrame in stack)
                {

                    MethodBase method = stackFrame.GetMethod();
                    if (IsTestMethod(method))
                    {
                        return Root + method.ReflectedType.Namespace + "." +
                               method.ReflectedType.Name + "\\" +
                               method.Name;
                    }
                }

                throw new ArgumentException("Not called within test framework.");
            }

            private static void ValidateStackFrames(StackFrame[] stack)
            {
                if(stack == null)
                {
                    throw new ArgumentException("Not called within test framework.");
                }
            }

            private static bool IsTestMethod(MethodBase method)
            {
                var attributes = method.GetCustomAttributes(typeof(TestMethodAttribute), false) as TestMethodAttribute[];

                return attributes != null && attributes.Length > 0;
            }


            ///
            /// Returns a folder unit to a running Unit Test. The same folder will be returned no matter how ofton it is
            /// called from within that test.
            ///
            /// A test-unique folder.
            /// If the test folder can not be generated.
            /// If the method is not called from within a Test Method.
            public string Create()
            {
                string folder = Folder();
                try
                {
                    if (Directory.Exists(folder))
                    {
                        Directory.Delete(folder, true);
                    }
                    Directory.CreateDirectory(folder);
                   
                }catch(Exception e)
                {
                    Assert.Inconclusive("Unable to build test folder " + folder + ", failed with " + e.Message);
                }
                return folder;
            }
        }
    }