Extract Embedded Resources With An Attribute In MbUnit

0 comments suggest edit

UPDATE: This functionality is now rolled into the latest version of MbUnit.

A long time ago Patrick Cauldwell wrote up a technique for managing external files within unit tests by embedding them as resources and unpacking the resources during the unit test. This is a powerful technique for making unit tests self contained.

If you look in our unit tests for Subtext, I took this approach to heart, writing several different methods in our UnitTestHelper class for extracting embedded resources.

Last night, I had the idea to make the code cleaner and even easier to use by implementing a custom test decorator attribute for my favorite unit testing framework, MbUnit.

Usage Examples

The following code snippets demonstrates the usage of the attribute within a unit test. These code samples assume an embedded resource already exists in the same assembly that the unit test itself is defined in.

This first test demonstrates how to extract the resource to a specific file. You can specify a full destination path, or a path relative to the current directory.

[Test]
[ExtractResource("Embedded.Resource.Name.txt", "TestResource.txt")]
public void CanExtractResourceToFile()
{
  Assert.IsTrue(File.Exists("TestResource.txt"));
}

The next demonstrates how to extract the resource to a stream rather than a file.

[Test]
[ExtractResource("Embedded.Resource.Name.txt")]
public void CanExtractResourceToStream()
{
  Stream stream = ExtractResourceAttribute.Stream;
  Assert.IsNotNull(stream, "The Stream is null");
  using(StreamReader reader = new StreamReader(stream))
  {
    Assert.AreEqual("Hello World!", reader.ReadToEnd());
  }
}

As demonstrated in the previous example, you can access the stream via the static ExtractResourceAttribute.Stream property. This is only set if you don’t specify a destination.

In case you’re wondering, the stream is stored in a static member marked with the[ThreadStatic]attribute. That way if you are taking advantage of MbUnits ability torepeat a test multiple times using multiple threads, you should be OK.

What if the resource is embedded in another assembly other than the one you are testing?

Not to worry. You can specify a type (any type) defined in the assembly that contains the embedded resource like so:

[Test]
[ExtractResource("Embedded.Resource.txt"
  , "TestResource.txt"
  , ResourceCleanup.DeleteAfterTest
  , typeof(TypeInAssemblyWithResource))]
public void CanExtractResource()
{
  Assert.IsTrue(File.Exists("TestResource.txt"));
}

[Test]
[ExtractResource("Embedded.Resource.txt"
  , typeof(TypeInAssemblyWithResource))]
public void CanExtractResourceToStream()
{
  Stream stream = ExtractResourceAttribute.Stream;
  Assert.IsNotNull(stream, "The Stream is null");
  using (StreamReader reader = new StreamReader(stream))
  {
    Assert.AreEqual("Hello World!", reader.ReadToEnd());
  }
}

This attribute should go a long way to making unit tests that use external files cleaner. It also demonstrates how easy it is to extend MbUnit.

A big Thank You goes to Jay Flowers for his help with this code. And before I forget, you can download the code for thiscustom test decorator here.

Please note that I left in my unit tests for the attribute which will fail unless you change the embedded resource name to match an embedded resource in your own assembly.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

15 responses

  1. Avatar for DotNetKicks.com
    DotNetKicks.com April 27th, 2007

    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  2. Avatar for Patrick Cauldwell
    Patrick Cauldwell April 27th, 2007

    That's awesome, Phil! I can't wait to try it out.

  3. Avatar for Jonathan de Halleux
    Jonathan de Halleux April 27th, 2007

    Pretty cool indeed.

  4. Avatar for Andrew Stopford's Weblog
    Andrew Stopford's Weblog May 10th, 2007

    Phil Haack has two great posts. A new fixture for embeding resources in tests , Phil has contributed

  5. Avatar for WPF Community Bloggers
    WPF Community Bloggers May 10th, 2007

    Phil Haack has two great posts. A new fixture for embeding resources in tests , Phil has contributed

  6. Avatar for Rob Cecil
    Rob Cecil May 17th, 2007

    I would suggest some small changes to your ExtractResourceAttribute.cs
    1. Decorating the ExtractResourceRunInvoker.Execute() with [System.Diagnostics.DebuggerNonUserCodeAttribute( )]
    (and others that may benefit)
    2. Change Execute() to:
    public override object Execute( object o, IList args )
    {
    Assembly assembly = attribute.Type.Assembly;
    using ( Stream stream = assembly.GetManifestResourceStream( attribute.ResourceName ) )
    {
    try
    {
    if ( String.IsNullOrEmpty( attribute.Destination ) )
    {
    ExtractResourceAttribute.st... = stream;
    return this.Invoker.Execute( o, args );
    }
    else
    {
    WriteResourceToFile( stream );
    }
    }
    catch ( TargetInvocationException tie )
    {
    if ( tie.InnerException != null )
    {
    throw new Exception( tie.InnerException.Message, tie.InnerException );
    }
    throw;
    }
    }
    try
    {
    return this.Invoker.Execute( o, args );
    }
    catch ( TargetInvocationException tie )
    {
    if ( tie.InnerException != null )
    {
    throw new Exception( tie.InnerException.Message, tie.InnerException );
    }
    throw;
    }
    finally
    {
    if ( attribute.ResourceCleanup == ResourceCleanup.DeleteAfterTest )
    File.Delete( attribute.Destination );
    }
    }
    Which means I never have to see TargetInvocationException unless there isn't a useful InnerException.

  7. Avatar for Rob Cecil
    Rob Cecil May 17th, 2007

    I would also make this change, otherwise, WriteResourceToFile( Stream ) below blows up with a null stream.
    [System.Diagnostics.DebuggerNonUserCodeAttribute( )]
    public override object Execute( object o, IList args )
    {
    Assembly assembly = attribute.Type.Assembly;
    if ( !new List<string>( assembly.GetManifestResourceNames( ) ).Contains( attribute.ResourceName ) )
    {
    throw new Exception( String.Format( "Unable to find embedded resource '{0}' in assembly '{1}'.",
    attribute.ResourceName, assembly.FullName ) );
    }

  8. Avatar for Haacked
    Haacked May 17th, 2007

    Hi Rob, thanks for the suggestions! I'll try and make sure they get into the next build.

  9. Avatar for Rob Cecil
    Rob Cecil May 20th, 2007

    One more change, Phil. This time allowing for the case where the resource might be organized into subfolders in the assembly. The .net assembly as you know converts subfolders into dots, so 'Results\test_results.xml' becomes '<typename>.results.testresults.xml'. Here is the Execute() method again, with changes:

    [System.Diagnostics.DebuggerNonUserCodeAttribute]
    public override object Execute( object o, IList args )
    {
    Assembly assembly = attribute.Type.Assembly;
    string resourceName = assembly.GetName( ).Name + "." + attribute.ResourceName.Replace( '/', '.' );
    if ( !new List<string>( assembly.GetManifestResourceNames( ) ).Contains( resourceName ) )
    {
    throw new Exception( String.Format( "Unable to find embedded resource '{0}' in assembly '{1}'.",
    attribute.ResourceName, assembly.FullName ) );
    }
    using ( Stream stream = assembly.GetManifestResourceStream( resourceName ) )
    {

    try
    {
    if ( String.IsNullOrEmpty( attribute.Destination ) )
    {
    ExtractResourceAttribute.st... = stream;
    return this.Invoker.Execute( o, args );
    }
    else
    {
    WriteResourceToFile( stream );
    }
    }
    catch ( TargetInvocationException tie )
    {
    if ( tie.InnerException != null )
    {
    throw new Exception( tie.InnerException.Message, tie.InnerException );
    }
    throw;
    }
    }
    try
    {
    return this.Invoker.Execute( o, args );
    }
    catch ( TargetInvocationException tie )
    {
    if ( tie.InnerException != null )
    {
    throw new Exception( tie.InnerException.Message, tie.InnerException );
    }
    throw;
    }
    finally
    {
    if ( attribute.ResourceCleanup == ResourceCleanup.DeleteAfterTest )
    File.Delete( attribute.Destination );
    }
    }

  10. Avatar for Haacked
    Haacked May 21st, 2007

    Hmmm... I'd almost rather require the tester to give the full resource name always. I use Reflector to find that out.

  11. Avatar for Haacked
    Haacked May 21st, 2007

    That avoids the issue of duplicate resources where the resource file name is the same, but the paths are different and the wrong one gets selected.
    Then again, maybe that's such a small risk as to not be concerned.

  12. Avatar for JIRA: MbUnit
    JIRA: MbUnit May 22nd, 2007

    Contributed by Phil Haack<br /><br />http://haacked.com/archive/... null

  13. Avatar for you've been HAACKED
    you've been HAACKED May 24th, 2007

    Motivate Your Unit Tests With the Release of MbUnit 2.4

  14. Avatar for Community Blogs
    Community Blogs May 24th, 2007

    Are your unit tests a little flat lately? Have they lost their shine and seem a bit directionless? Maybe

  15. Avatar for Paul Schofield
    Paul Schofield June 5th, 2008

    Has anyone gotten this working with MbUnit v3?