Tuesday, January 06, 2009

On demonstrating the value of unit tests to beginners

A little anecdote.  I'm slowly trying to get my team to wrap their heads around the idea that any new feature must be accompanied by unit tests as we near the point where they are absolutely required to write unit tests before they write code and the tests become part of the deliverable.  One thing I know is that if you haven't written a lot of unit tests yet, you don't really understand the value of them except very much in the abstract, so I am also trying to point out the value to him as we go.

One of my developers is writing a function that uses the compiler module to inspect some Python code, it's not really important why.  He had just learned how to write some unit tests in Javascript and I was teaching him how to use the stdlib unittest module for his unit tests written in Python.   His implementation isn't written yet, he is just starting to figure out what the interfaces look like, which is a great time to start writing failing tests.

I told him, start with this:

import unittest

class MyTestCase(unittest.TestCase):
  def test_myParseFunction(self):
    assert 0


.. and run that test, which he did, and it failed, and I said "good".  I started pointing out to him ways he could make very minor modifications to his code to make it easier to test (not nearly enough has been written on the subject of making code easier to test, BTW--bloggers, get on it!).

One of the improvements I asked him to make was to move some functions into another, more relevant module than the one he was working in.  He did that, then wrote version two of the unit test:

import unittest
import mynewmodule

class MyTestCase(unittest.TestCase):
  def test_myParseFunction(self):
    self.assertEqual(mynewmodule.myParseFunction("(python.code)"), [expected, output, structure])


Now, remember that he hasn't implemented myParseFunction yet.  So I asked him to run his failing test one more time.  And, a surprise to both of us, we got this:

[ERROR]: test_mymodule.MyTestCase.test_myParseFunction

Traceback (most recent call last):
  File "/usr/lib/python2.5/unittest.py", line 260, in run
    testMethod()
  File ".../test_mymodule.py", line 7, in test_myParseFunction
    self.assertEqual(mynewmodule.myParseFunction("(python.code)"), [expected, output, structure])
  File ".../mynewmodule.py", line 19, in myParseFunction
    return compiler.parse(s)
exceptions.NameError: global name 'compiler' is not defined


What the.. oh right!  He imported compiler in the original module, but forgot to move it to the new module.  The unit test - so far, one line long and expected to fail in a relatively uninteresting way, had already found a bug.  My favorite part was that it happened this way to someone learning why we put so much value on unit tests.

4 comments:

glyph said...

I think you still have to have a basic amount of buy-in on the concept before a demonstration like this can work.

I have had the experience of demonstrating the value of unit tests to a beginner in this way, and their response when we discovered a similar bug was "what is the point of writing all this extra code? of course I would have seen that error when I loaded the web page".

Unknown said...

I definitely agree with you, Glyph, and for that reason we've already had team-wide training on unit testing and we do a lot of talking about them, so he isn't totally green. This was more like letting him finally get traction on a concept he has already decided to pursue.

Anonymous said...

no point in extra coding

there should be some other automated annotated way

Dia said...

This should be taught long and hard the "why" in software development.
Aside from the flat forehead and less hair, save grief and do it.
So, portion of my certification includes a chunk of unit testing. the extra code and time spent saves a lot of headache across the board. thanks