Wednesday, December 3, 2008

On Python, unit tests and braiiiiiiiiiiinssss

This has been a hectic week. I've been staying too much time at work because there's some ... UGH EEW python UGH! work that I needed to finish.

And because bugs are really hard to catch in python, I've even been undersleeping trying to fix at home what I didn't fix at work.

A few surprises about Python.

  • The good: There's a command line. Just type "python" and a prompt appears.Wow. If C++ had a command line to test your own files, it'd be neat.

  • The bad: At the office, the team began googling for ways to deliver python binaries with all the dependencies resolved. So we learned about python eggs and easy_install. Congratulations to the developer who did that - but guess what? C++ already solves your dependencies for free. Why can't I just write my C++ program? Whatever, the pay is worth it ;-)

  • The ugly: Python's archaic attempt at error checking makes it NECESSARY that you not only use the interpreter I mentioned above, but also that you write unit tests to catch all the runtime errors that might come out in your program.
That leads me to my second topic for today: Unit tests.

Coding without Unit Tests is like playing Jenga(TM).

I've also realized that the way I've programmed all my life has been much less structured than I thought. Sure, my knowledge of separation of concerns, patterns, etc. has helped me a lot in coding - making my code heavily resistant to exceptions, memory leaks, etc. However, I haven't been always fond of writing tests for my programs. Which leds to writing HUGE chunks of code that are difficult to debug.

So what happens if you DON'T write unit tests? You'll end up adding temporary chunks of code like this one:


curpos = curpos + calculate_something;
# print "curpos right now is %d" % curpos
if curpos > len(s):
# print "there was an error in here!"
return None
else:
blablablah
some test code
additional test code
# commented test code
# more commented test code

# here's a huge chunk of temporary "debugging" code
here's a huge chunk of temporary "debugging" code
# here's a huge chunk of temporary "debugging" code
here's a huge chunk of temporary "debugging" code
here's a huge chunk of temporary "debugging" code
more code
more code
here's a huge chunk of temporary "debugging" code
# here's a huge chunk of temporary "debugging" code
more code
more code
# here's a huge chunk of temporary "debugging" code
# here's a huge chunk of temporary "debugging" code
more code
more code
more code
# here's a huge chunk of temporary code
# here's a huge chunk of temporary code
here's a chunk of temporary "debugging" code
here's a chunk of temporary "debugging" code
more code
more code
more code
more code
more code
more code
# another temporary test line
even more code
even more code
even more code
even more code
even more code
# here's old code, just in case
# here's old code, just in case
# here's old code, just in case
# here's old code, just in case
# here's old code, just in case
# here's old code, just in case
even more test code
even more test code
even more test code
clippy("hello there! Looks like you're \
trying to debug some code! Want some help?")

So THAT's what happens when you're not accustomed to writing unit tests. I know it very well, because that's how I've been programming for YEARS!
Just because you remove all that test code at the end, doesn't make it good code.

So, how do you write good code? I found out just yesterday.

When I rewrote the python program I was coding (a lightweight JSON parser, no less, which turned out to be completely unnecessary as I could write the configuration data using plain and simple .ini files, but I disgress), I suddenly decided to write simple use cases (about 10 or 20 sets of three-liners, which made up around 40% of the lines in my code) to see what was going wrong.

Here's more or less what I wrote:


def myfunc():
small chunks of code
small chunks of code
small chunks of code
small chunks of code

def myfunc2():
small chunks of code
small chunks of code
small chunks of code
small chunks of code

def myfunc3():
small chunks of code
small chunks of code
small chunks of code
small chunks of code

def myhugefunc():
huge chunks of code
huge chunks of code
huge chunks of code
if blablablah:
myfunc()
for blablablah:
myfunc2()
huge chunks of code
huge chunks of code
# And that's it!

def unittests():
# These are primitive tests that have to be examined
# by hand - but they're still light years ahead
# of the ugly Jenga(TM) code I posted above.
print "Testing KNOWN_INPUT"
print myfunc("KNOWN_INPUT")

print "Testing KNOWN_INPUT2"
print myfunc("KNOWN_INPUT2")

print "Testing KNOWN_INPUT3"
print myfunc("KNOWN_INPUT3")

print "Testing KNOWN_INPUT4 KNOWN_INPUT4 KNOWN_INPUT4"
print myfunc2("KNOWN_INPUT4 KNOWN_INPUT4 KNOWN_INPUT4")

print "Testing KNOWN_INPUT"
print myfunc2("KNOWN_INPUT")


So I ran the unit tests. Wham! Poof! Beef! Zonk! Suddenly, one after another, a horde of lemming-like runtime errors started appearing before my eyes. Wheeeeeeeeeeeeeeeeeee!

I also added some assertions (you knew there was a python AssertionError exception, didn't you? Ah, God bless the Code::Blocks IDE autocomplete, it's shown me some stuff about python I didn't even know) and was finally able to get my parser going. And guess what, turns out that the configuration data I was writing in the first place, had bad JSON syntax, and my program caught it, pointing at the exact line and position.

The moral of the story?

  1. Python sucks. Sorry, had to say it :P
  2. Write unit tests. They're easy to write, and they'll save you HOURS of debugging. No, i'm not kidding. I speak from experience.
  3. Use assertions. They look ugly in your code but they make your code act pretty - which is what matters.
  4. Divide your code in small chunks that can be unit-tested. If those chunks are used in various parts of your code, no matter how easy they are to code, you need to put them in their separate functions so you can test them with the unit tests. Yes, I'm writing it bold because it's THAT important.
  5. No more Jenga(TM) coding! Hurray!!
Now I need to go to sleep, my brain's entering zombie mode now. I have been sleeping 5-hours-a-day for the whole week.

Braaaaaaaaiiiiiiiiiiiiiiiiiiiiiinnnnnnnnnnnnns....

No comments: