lost-theory

The blog of Steven Kryskalla.


 

Spec runner using withhacks

written by stevek, on Mar 13, 2010 2:16 PM.

Here is an interesting video (and blog post) on Ruby vs. Python by Gary Bernhardt. One of the advantages Gary gives to Ruby is the ability to develop and use tools like rspec or cucumber, which use Ruby's block syntax for really nice looking unit tests, runnable specs, etc.

In the talk Gary shows that he was able to create a spec runner with syntax similar to rspec by using "really nasty" and "ugly" hacks (sys.settrace I believe). But, even if these techniques are ugly, they are becoming more easily accessible and more easy to develop with tools like withhacks, which abstracts the ugliness away.

I'm sure this does much less than what Gary's mote does, but here is a simple spec runner using withhacks:

from __future__ import with_statement

from withhacks import CaptureOrderedLocals, CaptureBytecode

class specs(CaptureOrderedLocals,CaptureBytecode):
    def __init__(self,what, *args, **kwargs):
        self.__what = what
        self.__args = args
        self.__kwargs = kwargs
        self.results = []
        super(specs,self).__init__()

    def __exit__(self,*args):
        retcode = super(specs,self).__exit__(*args)
        results = self.run_specs(self.locals)
        self.results = results

    def run_specs(self, cases):
        results = []
        num_pass, num_fail = 0,0
        print "Testing %s specs for %s:" % (len(cases), repr(self.__what))
        for (name, func) in cases:
            if not callable(func): continue
            name = name.replace('_', ' ')
            name = name.capitalize() + '.'
            print "->",
            try:
                func()
                error = None
                num_pass += 1
            except BaseException, e:
                error = repr(e)
                num_fail += 1
            if error:
                print "[FAIL]", name
                print "--->", error
                results.append((name, False, error))
            else:
                print "[pass]", name
                results.append((name, True, None))
        print "Result: %s/%s passed, %s/%s failed" % (num_pass, len(cases), num_fail, len(cases))
        print "-"*20
        return results

And here is an example spec:

class MyClass(object):
    def add(self, a, b):
        return a+b

with specs(MyClass):
    def it_adds_two_and_two():
        c = MyClass()
        assert c.add(2,2) == 4

    def it_adds_negatives():
        c = MyClass()
        assert c.add(10,-10) == 0

    def it_fails_adding_int_and_string():
        c = MyClass()
        try:
            c.add(10, 'foo')
        except TypeError:
            pass #correct!

    def testing_what_a_spec_failure_looks_like():
        c = MyClass()
        c.thisdoesntexist()

And here is the output:

Testing 4 specs for <class '__main__.MyClass'>:
-> [pass] It adds two and two.
-> [pass] It adds negatives.
-> [pass] It fails adding int and string.
-> [FAIL] Testing what a spec failure looks like.
---> AttributeError("'MyClass' object has no attribute 'thisdoesntexist'",)
Result: 3/4 passed, 1/4 failed
--------------------

I put the code on bitbucket here.

Comments

  • Nice idea! Thanks for sharing.

    Things would be a little bit easier if assert wasn't a statement but apparently there's a way to hack past that. :)

    I like how you forgo the default test prefix semantics (ie. test_add...). I think that the exception syntax could be perhaps a bit tidier, however. To me, the "pass" part strikes as ugly.

    Gary Bernhard's Expecter Gadget does this the following way:
    >>> with expect.raises(KeyError):
    ... insert code with expected error here. if it doesn't raise, expecter will give an AssertionError
    

    I think that's rather neat. Of course for simple statements you might just want do something along "expect(some statement raising an error).raises(KeyError)" but the idea is there.

    Have you planned adding set up and tear down to your implementation? This would help removing some redundancy altogether (ie. c = MyClass() ). In this case it might take some trickery but might be worth the effort.

    Comment by Juho Vepsäläinen — Mar 14, 2010 12:42:00 AM | # - re

    • Hi Juho. I think the expect.raises should be compatible with this. I believe nose also has some helpers for that sort of thing.

      You can get something similar to test setup by putting statements above the functions. For example I could have put c=MyClass() above the first function definition and then used in the next four functions. Tear down doesn't work this way though currently. But, since run_specs controls the execution of the cases it would be easy to add proper startup, shutdown, setup, and teardown. If I use this more I will be sure to add it :)

      Comment by stevek — Mar 14, 2010 1:23:26 PM | # - re

      • Good point about set up!

        If you want to add more explicit set up/tear down, you could do something along (ie. specs(MyClass, set_up, tear_down)) and decorate each spec at run_specs.

        I agree about the expect.raises point. It should indeed work just fine with the current implementation. I just find testing for exceptions a bit irritating, that's all. :)

        Comment by Juho Vepsäläinen — Mar 14, 2010 1:48:47 PM | # - re

  • I took your idea and refined it a bit further. See nixtu.blogspot.com/2010/03/speccer-early-prototype-of.html for an early prototype.

    Thanks for the inspiration. :)

    Comment by Juho Vepsäläinen — Mar 14, 2010 1:04:05 PM | # - re

    • Cool! I left you a comment.

      Comment by stevek — Mar 14, 2010 1:34:04 PM | # - re

  • Hullo Steven this is Hammerpants from the Polka Glocks and I do not understand any of this HAHAHA

    Comment by davy hamburgers — May 4, 2010 12:52:28 AM | # - re

    • Wow Davy Hamburgers on my blog!? A dream come true!!

      Comment by stevek — Jun 5, 2010 8:30:49 AM | # - re

Leave a Reply