Putting the Cart Before the Horse

On TDD, unit testing and version control

  As I have been growing my software skills I have heard the phrase, “Test Driven Development” (TDD) many times, but have never had a chance to explore it personally. For those who do not know what TDD is, it is a method for writing code that flips around the normal paradigm: tests before code instead of code before tests. For many developers, the development process starts with writing a chunk of code (often written in a manner not conducive to testing) and then applying tests (or not, for those who test in production) to see what worked and what didn’t. TDD is a paradigm that operates in reverse and forces the developer to write tests first, and then to write the code.

  In TDD, failure transforms from a dreaded possibility into an objective. The benefit of this is that it encourages code progression in a more concrete way as the tests are based off of the application’s requirements. This is compounded by how these tests are designed. TDD stresses bite-sized unit tests so that fixing bugs is easier as there is less room for things to go wrong.

  The complete cycle of development for TDD is “red”, “green”, and “refactor”. The red stage is where tests are made that are guaranteed to fail until a functional condition is met. Once it is made and fails properly, the next stage begins. In the green stage, the developer is tasked with fixing the bare minimum required to satisfy the test. Doing extra work increases the risk of having under-tested code, and having harder to fix issues later on.

  The final stage, “refactoring”, is where the tests and the production code can be optimized. The “refactoring” stage can involve removing redundant tests.  If you create a new test that checks a function’s output, then an earlier test that only asserts that the function can be called becomes unnecessary. Pruning these help keep the test cycle quick. Another example is finding sections of code that are common to multiple tests and making one copy accessible to all of them.

  In Python, I have been doing TDD with Pytest. In the previous example, where we need to deduplicate code, we can proceed in several ways, including using pytest fixtures, and xUnit setup and teardown functions. Pytest fixtures are decorators that can operate at class, module or session levels to give pre-defined information, or tools. They’re also a good example of dependency injectors, and can be used with the older xUnit functions that do a similar job if desired. 

As an example of Pytest and Pytest fixtures, consider this class that takes a dictionary and has a method that finds values by key prefixes (with corresponding test code): 

import pytest

class Foo(object):
   def __init__(self, my_dict=None):
      self.my_dict = my_dict or {}

   def find_values(self, starts_with):
      matching_values = []
      for key, value in self.my_dict.items():
          if key.startswith(starts_with):
              matching_values.append(value)
      return matching_values

# Creation of foo happens 3 times...we want to reduce this.
def test_non_existant_key():
    foo = Foo({'foo': 'bar', 'foobar': 'baz'})
    assert len(foo.find_values('hello')) == 0

def test_one_matching_key():
    foo = Foo({'foo': 'bar', 'foobar': 'baz'})
    values = foo.find_values('foobar')
    assert len(values) == 1
    assert 'baz' in values
    assert 'bar' not in values

def test_two_matching_keys():
    foo = Foo({'foo': 'bar', 'foobar': 'baz'})
    values = foo.find_values('foo')
    assert len(values) == 2
    assert 'bar' in values
    assert 'baz' in values

We can clean up the code a little bit by using a pytest fixture:

import pytest

class Foo(object):
   def __init__(self, my_dict=None):
      self.my_dict = my_dict or {}

   def find_values(self, starts_with):
      matching_values = []
      for key, value in self.my_dict.items():
          if key.startswith(starts_with):
              matching_values.append(value)
      return matching_values

# fixture function name must be the same as the argument
# to the test functions
@pytest.fixture
def foo():
   return Foo({'foo': 'bar', 'foobar': 'baz'})

def test_non_existant_key(foo):
    assert len(foo.find_values('hello')) == 0

def test_one_matching_key(foo):
    values = foo.find_values('foobar')
    assert len(values) == 1
    assert 'baz' in values
    assert 'bar' not in values

def test_two_matching_keys(foo):
    values = foo.find_values('foo')
    assert len(values) == 2
    assert 'bar' in values
    assert 'baz' in values

   The result of using these tools is that you make your tests quicker to run and easier to read. Another perk to using TDD, besides thorough testing, is that it gives comfortable checkpoints for version control. While completing an online course on TDD, commits felt organic after each cycle or two. While this may not be practical for larger projects, making a commit after a small number of tests would still be very useful. Operating like this gives a steady pace to work at. This helps avoid a tendency of writing bloated sections that may need to be re-written if part of them isn’t needed.

   While getting used to this new paradigm can feel odd, it has definite benefits for projects of any size. A good rule of thumb is that If the project is big enough to use version control, then it’s a suitable candidate for TDD. As with any skill, practice makes perfect and TDD is no exception. I was pleasantly surprised at how the TDD process improved my development cycles, and believe there’s a lot of value in using it in most programming ventures to help create clean, maintainable and correct code.

Further Reading

Uncle Bob Martin’s Three Rules of TDD

Test Driven Development: By Example, Kent Beck (2002)

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s