One of the goals of my metaframework was to let the developers write their own Selenium tests. Since we’re a java shop, this means it has to support JUnit. My boss wanted me to write the whole framework in Java, but well, I like Python better. Realizing that I’m likely not going to convince the entire development team to switch from the dark side to Python, I did it in Jython.

Here is the cut is the annotated Jython code for compiling, running and getting the results of a JUnit test.

The JUnit jar is in my classpath already, so Jython thinks it is just another module

<pre lang="python">import junit

I always compile the tests. No doubt there is a way to only do this when necessary, but these are Selenium tests, so speed is already not much of a concern. Here I build up the path to javac, set some classpath stuff and open a shell to do the compile. It all very windows specific, but wouldn’t be that hard to make it platform safe. The reason I’m using os.path.join alot is because I’m using Sync ‘n Run. On the list of “fun” things to do is replace the popen with a “pure” java compile solution, but this works.

<pre lang="python">javac = os.path.join(framework_root, "platform", platform, "jdk1.6.0_03", "bin", "javac.exe")
classpath = "%s" % (os.path.join(framework_root, "platform", "all", "junit", "junit-4.4.jar"))
classpath = "%s;%s" % (classpath, os.path.join(framework_root, "platform", "all", "selenium-rc", "java", "selenium-java-client-driver-0.9.2-SNAPSHOT.jar"))
p = popen2.system('%s -classpath %s %s' % (javac, classpath, f))

Now that we have a compile test, we can import it as a module by just removing the .class portion of it’s filename.

<pre lang="python">f_parts = os.path.splitext(f_name)
try:
    t = __import__(f_parts[0])
except ImportError:
    print "could not import %s" % f_name
    aggregates["error"].append("%s" % f)
    continue

We need something to hold the result

<pre lang="python">tr = junit.framework.TestResult()

First we get all the test methods in the test class, then we remove the ones we don’t want. One of the arguments to the framework is a test pattern which has two parts. The first is a wildcard to determine the file(s) that should be run and is always provided. Optionally you can include a specific test that you want to run by appending .<test> to the pattern.

<pre lang="python">tmp_suite = junit.framework.TestSuite(t)

if args[0].find(".") != -1:
    suite = junit.framework.TestSuite()
    t_name = args[0].split(".")[1]
    tmp_tests = tmp_suite.tests()
    while tmp_tests.hasMoreElements():
        tmp_test = tmp_tests.nextElement()
        if tmp_test.name == t_name:
suite.addTest(tmp_test)
else:
    suite = tmp_suite

Now we actually run the tests — assuming there are any of course.

<pre lang="python">if suite.countTestCases() > 0:
    suite.run(tr)

My lack of Java prowess shines through here where I convert a bunch of enums produced by the running of the test into lists which I know how to deal with much better.

<pre lang="python">    all_tests = []
    _all_tests = suite.tests()
    while _all_tests.hasMoreElements():
        all_tests.append(_all_tests.nextElement())
    
    my_failures = []
    _my_failures = tr.failures()
    while _my_failures.hasMoreElements():
        my_failures.append(_my_failures.nextElement())
    
    my_errors = []
    _my_errors = tr.errors()
    while _my_errors.hasMoreElements():
        my_errors.append(_my_errors().nextElement())

Since this is a metaframework, I need to track results from lots of different things so I have a dictionary called ‘aggregates’ which I use for that purpose. Here I populate the appropriate places.

<pre lang="python">    if (suite.countTestCases() - tr.failureCount() - tr.errorCount() > 0):
        for s in all_tests:
            if s not in my_failures or s not in my_errors:
                aggregates["pass"].append(s)

    if tr.failureCount() > 0:
        for fa in my_failures:
            x = fa.failedTest().toString()
            aggregates["fail"].append("%s:%s" % (f, x[:x.find("(")]))

    if tr.errorCount() > 0:
        for e in my_errors:
            x = e.failedTest().toString()
            aggregates["error"].append("%s:%s" % (f, x[:x.find("(")]))

And I clean up after myself. Clearly this line would go away if I worked on only compiling when necessary.

<pre lang="python">os.remove(f.replace(".java", ".class"))