Feed on
Posts
Comments

Django has wonderful unit testing functionality built into the framework. It is often ignored because Django gives so much functionality “out of the box”, but this doesn’t mean that one shouldn’t do “their part” when extending the framework to build your own applications. Tests are a great way to “figure out” what needs to be done, then actually taking what you’ve learned to implement in the application proper.

My attempt here is to show how I used TDD in an Django application. We’ll create a basic project with a single application and use unit testing to figure out an algorithm an implement it in the application’s class.

Why Test at All?

I’ve spoken to many groups about Test Driven Development (TDD). Many experienced engineers don’t do it because they haven’t been indoctrinated into the practice early, and they are established in an organization where they maintain and extend code that they might have been writing for months or even years. The code has been fully tested and it is solid. Why write unit tests?

Newer developers don’t know how to start with tests. They write code, then write tests, then get frustrated because they are continuously writing code twice. Corners get cut, and at best you get the “bare minimum” of coverage, which is basically what the boss sees and reviews.

Both of these scenarios are unfortunate, because there are a few things that TDD gives gives all engineers and the organizations that they work for:

  • More efficient QA. QA engineers and testers don’t have to test for functionality, and they get better quality code.?What does this mean? It means that they get to do the part of their job that is more meaningful to the company as a whole. If the basic code and functionality are automatically tested, QA can then find the border conditions and those evil underlying non-obvious bugs that can kill an application, and if large enough, kill a company’s profits.
  • Better and more cohesive organizations. Test Driven Development builds confidence throughout the entire organization. Engineers working often in geographically dispersed groups need to “trust” each other, their skills and abilities. If everyone in the organization is using TDD, these groups will know that the quality coming from others will work. This not only creates trust amongst engineers, but throughout the organization, from Marketing and Business resources all the way through QA.
  • Professionalism. Being a “Pro” doesn’t mean generating thousands of lines of code.?It means generating efficient, simple to maintain, easily configurable code that “just works” every time. It not only has to work, it has to be tested and documented.
  • Peer pressure to “do the right thing”. If the senior members and thought leaders of an engineering group are all using TDD, documenting their code and communicating effectively, this will spread throughout the organization and foment a best practices environment.

Test Driven Development with high unit test coverage creates efficient, spirited and profitable software. The balance of this post will explain just how I use TDD to implement a complex Bank Routing Number (ABN) validation class. I am assuming that the reader is familiar with Django, and has a comfortable understanding with basic Django and Python development principles:

Create the Project and Application

My application’s name is

valid_lib

It is destined to be a Service Platform that allows all kinds of complex financial form data to be validated through a ReSTful interface. At the moment I’m not worried about the Service, the Interfaces or even the Business Model. Currently, I want to get all the validation algorithms and calculations in place. The rest of the stuff is pretty easy. At the command line, in my Django development directory, I type:

django-admin.py startproject valid_lib

To start my project. I cd into the valid_lib directory and then type:

django-admin.py startapp finvs

To create my application and it’s directory. At this point I add the valid_lib.finvs to my INSTALLED_APPS in the the settings.py file, and while I’m there I add my database information, even though I really won’t be using it yet. I also add another folder in the finvs directory that I name util. Run a manage.py syncdb against the application’s root and you should be good to go. Everything should now look like fig 1:

fig 1: Initial TDD validation app structure

(I’m writing this on my MacBook Pro and using Textmate, but you should be getting similar results however you’re doing it)

Notice that there is a test.py file in the finvs directory. This is where we’re going to start. Django’s default unit testing fixture looks for this file and will execute tests within it. We’ll open that file up and begin writing our code there.

ABN Validation

ABA (American Banker’s Association) routing numbers (ABNs) are used to identify financial institutions when making transactions. This number is usually required when setting up a bank account for direct deposits, automated payments, etc. Or when making payments by “paperless” check over the phone or online.

These routing numbers include a checksum digit which can be used to help verify if a given number is valid or not.

The algorithm checks the first second and third number and adds them together, then iterates through every three numbers (total of 9) to return a value. If the resulting sum is an even multiple (think modulus here…) of ten (but not zero), the ABA routing number is good. ?We’ll now write a test with a valid ABN to see if we can get things to work correctly. In the tests.py file, write:

"""
This file tests the finvs application
"""
import unittest
from django.test import TestCase

#Here are some bank numbers that we can run tests against...
SHORT_BAD_ABN_NUM = 1000012
LONG_BAD_ABN_NUM = 255073345999
BAD_ABN_NUM = 255073545
GOOD_ABN_NUM = 255073345

class ValidatorsTestCase(unittest.TestCase):

    def setUp(self):
        self.value = CITY_CODE_FRAUD

    def testAbnAlgorythm(self):
        """Tests the algorythm with a known good bank number.
           This test is here to create the algorythm to be used
             in the validator implementation.  When it's right,
             cut and paste it into the validator class.

           The algorithm checks the first second and third number
            and adds them together, then iterates through every
            three numbers (total of 9) to return a value.

           If the resulting sum is an even multiple of ten
            (but not zero), the ABA routing number is good.
           """
        n=0
        bank_str = str(GOOD_ABN_NUM)
        num_length = len(bank_str)
        if (num_length ==9):
            for j in range(0,num_length,3):
                t = int(bank_str[j]) #this is the first digit
                ti = int(bank_str[j + 1]) #this is the second digit
                tii = int(bank_str[j + 2]) #this is the third digit
                n += (t * 3)+ (ti * 7)+ tii #add them up
            #check for zero and modulus of 10
            print((n != 0) & ((n % 10) == 0))  #we'll take this line out when we get it right.
            self.assertTrue((n != 0) & ((n % 10) == 0))

Now, at the application root, run:

python manage.py test finvs.ValidatorsTestCase

You should get something very similar to this:

$ python manage.py test finvs.ValidatorsTestCase
Creating test database...
Creating table auth_permission
Creating table auth_group
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Installing index for auth.Permission model
Installing index for auth.Message model
True
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
Destroying test database...

The True that you see in the output is your print statement. Notice that the system also creates and destroys a test database. This is extremely cool functionality that can be used with Django’s Fixtures, but we’re not going to use it here for this code set. We haven’t written a line of production code, but our test passes, and if you look at the test, it is the basis for the actual implementation!

Implement It!

We’re really close, but we’ll need to make a few changes before we implement it in our “production” code. Seriously, we’re really close!

In the utils directory, create the file validation_formulas.py This is where we’ll add our ABN validation class. In my test code I created a check to ensure that the number sent in had exactly 9 digits. I did this because I didn’t want to have a try / except block, and to keep it clean. Feel free to add this if needed. The other change is that we actually need to return something — True or False in this case. The test code’s def doesn’t return anything, it just makes an assertion. To validate something, we need to insert a parameter into the method and then return a true/false. We also need to give it a name. In the validation_formulas.py file, you can now insert:

class ValidationFormulas:

    def isBankRoutingNumber(self,bankNumber):
        """Parses the bank routing number for purposes of finding a fraudulent entry.
            the algorithm check the first second and third number and adds them together,
            then iterates through every three numbers (total of 9) to return a value.

           If the resulting sum is an even multiple of ten (but not zero), the ABA
            routing number is good.

           param -- bankNumber the 9 digit bank number;

           return boolean true if number is valid, false if not. false is the default
           """
        n=0
        retbool=False
        bank_str = str(bankNumber)
        num_length = len(bank_str)
        if (num_length ==9):
            for j in range(0,num_length,3):
                t = int(bank_str[j])
                ti = int(bank_str[j + 1])
                tii = int(bank_str[j + 2])
                n += (t * 3)+ (ti * 7)+ tii
            retbool = (n != 0) & ((n % 10) == 0)
        #return the value
        return retbool

Notice the added documentation. Some of it is cut and paste from the Test, the rest is information that you’ll want to convey to anyone that is using this code in the future, because hopefully at some point the company you work for will realize just how brilliant you are and promote you to CTO, where you won’t get to write code anymore. You sure as heck don’t want to leave the poor schlep that is hired to take your place in the dark!

Also notice that I have a default return of False some applications may need a different default return, or this return may be handled in a manager class that registers the default returns in some kind of persistence layer that is more dynamic. Your mileage may vary.

Wrapping it Up

There it is. You’ve written a test first. You’ve used the implementation of the test and created a sensible implementation. If there are other ways of refining your code or cleaning things up, you can always go back and make more changes. But your’e not done. You need to write more tests to actually verify that this algorithm works, that it shows false for bad routing numbers, and that it an spot when the length of the number is incorrect without throwing any index out of bounds exceptions or the like. you can then add more test methods to your ValidatorsTestCase class. I’ve also leveraged the setUp method that is provided with the framework to generate my constants for me. The code will then look like this:

"""
This file tests the finvs application
"""
import unittest
from django.test import TestCase
from valid_lib.finvs.utils.validation_formulas import ValidationFormulas

class ValidatorsTestCase(unittest.TestCase):

    def setUp(self):
        self.SHORT_BAD_ABN_NUM = 1000012
        self.LONG_BAD_ABN_NUM = 255073345999
        self.BAD_ABN_NUM = 255073545
        self.GOOD_ABN_NUM = 255073345

    def testAbnAlgorythm(self):
        """Tests the algorythm with a known good bank number.
           This test is here to create the algorythm to be used
             in the validator implementation.  When it's right,
             cut and paste it into the validator class.

           The algorithm checks the first second and third number
            and adds them together, then iterates through every
            three numbers (total of 9) to return a value.

           If the resulting sum is an even multiple of ten
            (but not zero), the ABA routing number is good.
           """
        n=0
        bank_str = str(self.GOOD_ABN_NUM)
        num_length = len(bank_str)
        if (num_length ==9):
            for j in range(0,num_length,3):
                t = int(bank_str[j])
                ti = int(bank_str[j + 1])
                tii = int(bank_str[j + 2])
                n += (t * 3)+ (ti * 7)+ tii
            self.assertTrue((n != 0) & ((n % 10) == 0))

    def testBankRouter(self):
        """Tests the algorythm in the validator against
              known good and bad bank numbers.
           """
        bank_num = self.SHORT_BAD_ABN_NUM
        formt = ValidationFormulas()
        bool = formt.isBankRoutingNumber(bank_num)
        self.assertFalse(bool)

        bank_num = self.LONG_BAD_ABN_NUM
        formt = ValidationFormulas()
        bool = formt.isBankRoutingNumber(bank_num)
        self.assertFalse(bool)

        bank_num = self.BAD_ABN_NUM
        formt = ValidationFormulas()
        bool = formt.isBankRoutingNumber(bank_num)
        self.assertFalse(bool)

        bank_num = self.GOOD_ABN_NUM
        formt = ValidationFormulas()
        bool = formt.isBankRoutingNumber(bank_num)
        self.assertTrue(bool)

I’ve added an import for the “production” class that i’m testing (ValidationFormulas). Finally, I’ve added a new method that implements new tests against the actual methods in this class to check my work. Running the tests now reveal:

Creating test database...
Creating table auth_permission
Creating table auth_group
Creating table auth_user
Creating table auth_message
Creating table django_content_type
Creating table django_session
Creating table django_site
Installing index for auth.Permission model
Installing index for auth.Message model
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK
Destroying test database...

If someone in the future messes with the “production” code implementation and things get out of whack, It will break here instead of in front of the user. By combining this with a continuous-build environments, a lot of mistakes can be caught extremely fast, sometimes within seconds of a source control check in!

Enjoy.

   Send article as PDF   

3 Responses to “Test Driven Development with Django Unit Testing”

  1. Rolf Brandt says:

    Thanks for your “testing with django” in a nutshell. I’ve learned how to test with django’s implemented facilities but one thing is still vague. I am aware of the importance to develop test driven, but i haven’t come to bring it to reality and so i am still lost about what exactly to test, when it comes to developing a django project. Your example is pretty straight forward but somehow still not directly affiliated with django. Are there any things one has to test specifically when developing with django, like redirects etc.? How would you proceed when tackling database dependent functions?

  2. Danilo Gurovich says:

    i believe I would test through the model at first. I’m still on the fence with fixtures as true “unit tests”, although they are tremendously valuable, but kinda clunky. I don’t think that unit tests are platform specific; it is more important to test things that affect the functionality and performance of the application, any application on any platform, and not be too worried about an affiliation with a platform.

    I plan to experiment more with django’s fixtures packages, but time has been at a premium lately. I hop this helps. If you have further questions don’t hesitate!

  3. Stu says:

    Hi,
    It looks like you didn’t define CITY_CODE_FRAUD, should this be None ?

    Cheers

Leave a Reply

Bad Behavior has blocked 80 access attempts in the last 7 days.