Monday, April 14, 2008
Someone recently sent me an email letting me know my NUnit "support" for IronPython project only targeted NUnit 2.2 and was a little out of date. 

(For those not sure what I'm talking about - perhaps take a look back at these posts (1, 2) from October of 2006)

I didn't realise anyone was actually using it - but I see it's being linked too from the IronPython cookbook, so I decided to take half an hour and update it to target NUnit 2.4 - I left it targeting IronPython 1.1 however instead of 2.0 (IronPython users should be able to handle that task if required).

The usage changes a little from NUnit 2.2 to 2.4, instead of deriving your tests from a custom TestSuite class you now just annotate an empty class with some attributes... makes everything a little cleaner as your project doesn't need to reference IronPython etc. any more.

[PythonSuite, Script(FileName = "MyPythonFixture.py")]

public class MyPythonSuite

{

}


The python files are embedded resources by default (though it's a minor code change to support external python files as well) - you can include multiple Script attributes for a single suite as well, if required.

If you want to dynamically include python resource files based on their content i.e. all embedded resources with a first line containing "#test" then you would do this:

[PythonSuite(DiscoverEmbeddedResources = true, DiscoveryKey = "#test")]

public class DynamicPythonSuite

{

}


The project is now split into two assemblies.
  • IronPythonTest.Addin
  • IronPythonTest.Framework
The Addin assembly needs to be installed into your NUnit 2.4 add-ins directory i.e. C:\Program Files (x86)\TestDriven.NET 2.0\NUnit\2.4\addins - you will need to place the IronPython assemblies in there as well if they're not registered in the GAC.

Only the framework assembly needs to be referenced in your project so you can have access to the PythonSuite and Script attributes.

And that's about it, all other questions should be answered in the posts from 2006, or just leave a comment on this post if you have any trouble.

Code can be downloaded from here.

posted @ Sunday, April 13, 2008 9:25:23 PM (New Zealand Standard Time, UTC+12:00)    Comments [0] | Trackback |
 Sunday, October 08, 2006
Back again!

Here is the source for the last post: ironpython-and-nunit.zip (23.01 KB) - it's pretty rough around the edges (and in the middle :P) because I was just trying to see if it was possible, rather then to produce some production-level toolkit for integrating IronPython and NUnit.

Edit 2008-03-14: An updated version of the code target NUnit 2.4 and VS2008 can be found here.

Usage

First off... usage is pretty straightforward.
  • Test fixtures must have NUnitFixture as one of their base classes.
  • Test methods must be prefixed with "test".
  • The setup method, if you need one, must be called "setUp"
  • The teardown method, if you need one, must be called "tearDown"
  • Fixture setup and teardown methods are also supported, they are called "setUpFixture" and "tearDownFixture".
  • Assert.XXX methods are the same as NUnit, unless you want to assert an exception is thrown.
  • For asserting an exception is thrown, make a call to "self.failUnlessRaises(...)" - it has the same syntax as the PyUnit equivalent, however it's tweaked slightly so it will work with Python exceptions, or CLR exceptions.
  • Do not perform an "Import * from System" in your test fixtures - it will clober the native Python Exception type, and may give you odd behaviour.


Implementation

There are 8 classes and one python script which make up the implementation, the 8 classes are split between the "model" (basically the python-side of the equation and the "NUnit extension" which are the necessary extensions to expose the model to NUnit...) - let's run through it quickly:

First we have the AbstractPythonSuite which hosts the python engine, and handles configuring and running the scripts containing the test fixtures.  Then we have a couple of classes which form the "model" consisting of a PythonFixture, representing a fixture, and PythonTestCase, representing a single test method in the fixture.

To build the model we have a PythonFixtureBuilder which is passed the python engine (from within the AbstractPythonSuite) and spits out all the fixture models, with the test cases assigned... this also assigns the various delegates for the setup/teardown/setupFixture/teardownFixture methods.

At this point the model is complete, it's just a matter of integrating it with NUnit - for this we have some more classes.

First of we need the PythonSuiteExtension, which represents the overall test suite, this contains PythonFixtureExtension's, for the fixtures, and finally PythonTestMethod for the individual test methods - the python extensions take care of running the tests in the model and calling the setup and tear down methods at the right times, and recording the results of course.

Last of all we have the PythonSuiteExtensionBuilder as mentioned in the last post, which takes care of creating the PythonSuiteExtension's for each class derived from AbstractPythonSuite it discovers in the target assembly.

Last of all we have a small python script which provides the necessary implementation for the base NUnitFixture class that all test fixtures are derived from... and a helper method we use in the PythonFixtureBuilder to identify the test methods in a class.

class NUnitFixture:

    def __init__(self):

        pass   

    def setUp(self):       

        pass       

    def tearDown(self):       

        pass

    def setUpFixture(self):       

        pass

    def tearDownFixture(self):       

        pass

    def failUnlessRaises(self, excClass, callableObj, *args, **kwargs):       

        if issubclass(excClass, System.Exception):

            try:

                callableObj(*args, **kwargs)

            except Exception, e:               

                if hasattr(e, "clsException") and (type(e.clsException) == excClass):

                    return

        else:

            try:

                callableObj(*args, **kwargs)

            except excClass:

                return

        if hasattr(excClass,'__name__'):

            excName = excClass.__name__

        else:

            excName = str(excClass)               

        raise AssertionException("%s not raised" % excName)

 

def getTestCaseNames(testCaseClass, prefix):

    def isTestMethod(attrname, testCaseClass=testCaseClass, prefix=prefix):

        return attrname.startswith(prefix) and callable(getattr(testCaseClass, attrname))

    testFnNames = filter(isTestMethod, dir(testCaseClass))

    for baseclass in testCaseClass.__bases__:

        for testFnName in getTestCaseNames(baseclass, prefix):

            if testFnName not in testFnNames:

                testFnNames.append(testFnName)

    testFnNames.sort(cmp)

    return testFnNames


And that's all there is to it...  If anyone actually finds this useful and wants to build on from it, please feel free, and if at all possible keep it open source (though you don't have to...)

Something to keep in mind is that at the moment if your test fixture can't actually be parsed and executed by the python engine, that exception will bubble up, eventually stopping the test assembly from being loaded at all... I think the nicer solution to this would be to add a custom NUnit test case for each script that failed to load, which in turn fails when executed as part of the assembly - dumping out the exception raised during construction.

At least then a tool like TestDriven.Net would quickly notify the developer of their mistake, instead of just falling over.

And very last of all, though I've been manually specifying the names of the script files.. using a class declaration like this:

public class MyPythonSuite : AbstractPythonSuite

{

    public MyPythonSuite()

        : base("MyPythonFixture.py")

    {

    }

}


You could make it a little more dynamic and do something like this:

public class DynamicPythonSuite : AbstractPythonSuite

{

    public DynamicPythonSuite()

        : base(FindSuitablePythonScripts())

    {

    }

 

    private static string[] FindSuitablePythonScripts()

    {

        List<string> scripts = new List<string>();

        Assembly assembly = typeof (DynamicPythonSuite).Assembly;

        string indication = "#test";

        foreach (string potentialScript in assembly.GetManifestResourceNames())

        {

            using (StreamReader reader =

                new StreamReader(assembly.GetManifestResourceStream(potentialScript)))

            {

                if (reader.ReadLine().Trim().StartsWith(indication,

                    true, CultureInfo.InvariantCulture))

                {

                    scripts.Add(potentialScript);

                }

            }

        }

        return scripts.ToArray();

    }

}


Which will load all scripts with "#test" in the first line, which seem'is a little easier to me (not having to spell out the path to the script, and only having to create the test file to make things happen) - YMMV of course.
posted @ Sunday, October 08, 2006 8:52:32 AM (New Zealand Daylight Time, UTC+13:00)    Comments [0] | Trackback |
Search
FeedCount

Tags...
Who am I?
Alex Henderson
Alex Henderson
Auckland, New Zealand
Managing Director at Dev|Defined Limited

"Self Confessed Coding Junky for 15 years"
View Alex Henderson's profile on LinkedIn
 
Mobile: +64-21-402-969
Email: bittercoder 'at' gmail 'dot' com
MSN: bittercoder_nz@hotmail
Skype: alex.devdefined
Navigation