Sourcode for "Integrating NUnit & IronPython"
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()
{
Listscripts = new List ();
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.