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 | Tracked by:
"IronPython & NUnit - Updated For NUnit 2.4" (Bitter Coder) [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