A Tried and Tested True Believer

I was first introduced to the concept of testing by Killer Web Development. I sort of understood it, but at first glance it sounded ridiculous. How can I possibly write an app to test an app that doesn’t exist yet? I thought computer programmers were logical people! And doesn’t this double my work, having to write two apps instead of one? I honestly didn’t fully grasp it. Who has time to write tests? All I have to do is run my app and look at it myself and see if it does what I want..should be simple, right? I want to crank out my code and see results NOW! So, what changed my mind? My work on CsvChamp, that’s what. I found myself tediously opening CSV files, running them through my module and looking at the output. I got weird results I didn’t understand, and my brain seemed to freeze up trying to figure them out. I spent what seemed like hours staring at my screen, trying to untangle the convoluted flight path my data took as it crashed and burned. Somewhere I read something about testing that spoke to me right where I was: wouldn’t it be great if I could have all this work done by….Python? That got my attention. Back to Google…

Thanks to this chapter of Dive Into Python, I now have my very first snippet of “tested first” code. It’s my Python 3 revision of my column alignment function in CsvChamp. Following along with the example, I started off with a blank code file. As I tried to figure out how to write my test, lo and behold — I found myself thinking much more clearly about what I wanted the function to do and what would constitute valid input. I also liked the idea of Python doing my work for me! Unlike before, when I’d run the function again and again on live data and view the results myself to see if they looked OK, I only had to run it through a unit test, with data I already knew was right, and have the test results display a happy green “ok” symbol in my console. After a few go-arounds, here’s the result:

import csv

# Define exceptions
class CsvChampError(Exception): pass
class WidthError(CsvChampError): pass
class AlignmentSpecError(CsvChampError): pass


def StrAlign(text, width, align):
    """Returns text aligned within column according to alignment specified"""

    # Width must be at least 1 more tha len(text) so there will be at least
    # one space between columns.
    if width <= len(text):
        raise WidthError("Invalid width. Must be > length of text.")

    # Padding = total number of blank spaces
    padding = width - len(text)

    if align == "L":
        aligned = text + (padding * " ")

    elif align == "R":
        aligned = (padding * " ") + text

    elif align == "C":
        # Divide padding by 2 and round down to nearest integer
        spaces_before = int(padding/2)

        # Put the rest of the spaces after the text
        spaces_after = padding-spaces_before

        aligned = (spaces_before * " ") + text + (spaces_after * " ")
    else:

        raise AlignmentSpecError("invalid alignment spec (must be L, R or C).")

    return aligned

For comparison’s sake, here’s the old version of the same function:

def FormatCsvCol(width, align, cont):
    """
    Returns a column of data with content aligned within an allotted width'
    """
    column = ""
    # The total number of empty spaces in the column.
    spaces = width - len(cont)

    if align == "L":
        # Place all the spaces after the content
        column = cont + (spaces * " ")

    elif align == "R":
        # Place all the spaces before the content.
        column = (spaces * " ") + cont

    elif align == "C":
        # Divide the number of spaces by 2 and round down to the nearest
        # integer.
        # Put the content between this number of spaces, and the rest.
        sp_before = int(spaces / 2) * " "
        sp_after = (spaces - len(sp_before)) * " "
        column = sp_before + cont + sp_after

    return column

Obviously, the big difference is the addition validity checking and informative error messages when invalid input is received. This was a direct result of testing using Dive Into Python’s example, forcing me to look at the big picture. This function could end up anywhere: It could be used to display neat columns of data in a console, in a GUI widget or in a text file. The deeper it gets buried in a complicated app, the more crucial it is to not just know invalid inputs somehow crept into it, but WHY those inputs were invalid. Now I know the function works, without having had to keep feeding it actual CSV data and repeatedly viewing text files myself. Here’s the unit test I came up with:

"""Unit test for CsvChamp3"""

from CsvChamp3 import StrAlign
import unittest

"""
Function StrAlign to align a string within a column of given width, according
to an alignment specification of left, center or right.
"""
class StrValues(unittest.TestCase):
    StrValues = (("example",10,"L","example   "),
                 ("example",10,"C"," example  "),
                 ("example",10,"R","   example"),
                 ("platypus",12,"L","platypus    "),
                 ("boomerang",15,"R","      boomerang"),
                 ("bubba",13,"C","    bubba    "))

    def testValidValues(self):
        """StrAlign should give known results with known values"""
        for text, width, align, text_out in self.StrValues:
            result = StrAlign(text,width,align)
            self.assertEqual(text_out, result)

if __name__ == "__main__":
    unittest.main()

Of course, I still need to progress to testing invalid input, and functional testing. But this is certainly an encouraging start.