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.