Explain this to me as if I was a small child – Developer Test Driven Development

There are so many ways to do testing. It all depends on the technology, the framework, and what automation tools your organization is willing to invest in. Automated testing is a great way to assert sanity in your build, especially when multiple people are touching the same segments of code. Two main approaches to testing here are Behavior Driven Development, which is usually done for acceptance testing, and Test Driven Development, which is used by developers to validate specification. This write-up will cover the latter.

So you’ve been told to write unit tests with a Test Driven Development (TDD) approach. What does that mean? You’ve read that you write a little code, and test a little code. You may have read you write all you tests first (daunting!). Sounds foreign to you, so you say, “screw it, I’ll use the debugger like I usually do.” Well, if you write good unit tests, you probably won’t ever need a debugger, and in the chance you actually need to run the debugger, it’ll get you to what you need much quicker.

How do you do it? Enter the xUnit the framework. xUnit is simply a loose collection of testing frameworks modeled off of the original SUnit framework used with SmallTalk. Just about every language has a clone of this framework you can use, e.g. jUnit for Java and Test::Class for Perl. In this example, we’ll be using unittest.py for Python.

So let’s get to some code. Let’s say we’re trying to write a rudimentary encryption library. We’ll use something stupid easy, like ROT13. (Note, don’t ever use ROT13 as an encryption format). Let’s start by creating a blank class.

Now at this point, believe it or not, you’re ready to write your first test. Why would you write a test for something that doesn’t have much content? There are many reasons. First, it’s to make sure you spelled things right. Sure, in the age of IDEs, this isn’t supposed to happen, but it can if you have to make a one-off change in a text editor. Second, it can teach you things about a new language, like, “Does this language provide a default constructor?”

And third, which I think is most important, this allows you to define the API, or at least put down some semblance of a specification that you’re given. The interface should, more often than not, be dictated by how the library is going to be used, not by the data store or back end technology that it wraps.

Let’s start with the basic test stub and add a basic test. We’ll say that we want to at least be able to instantiate a ROT13 object and print out the ciphertext.

Our custom test class inherits from unittest.TestCase, which gives us access to a bunch of other methods we’ll come across later. The test method, starting with test, will get executed every run. The final line in the main body simply executes all test cases inheriting TestCase. Let’s run it to see what happens.

Hmm, okay, it passed. This tells us two things: Python provides us with a default constructor, and it also provides a default string representation. Now, the default string output is not quite what we wanted. Let’s define our interface such that we have a constructor that takes in a plaintext, and our string representation gives us the ciphertext. We’ll keep our old test to make sure our old behavior still works, but we’ll add a new test to confirm our new behavior:

Running our test again, we’ll see that it fails.

Now, we can go back to our class and an optional parameter so that we can satisfy both tests.

Running our tests again, we’ll see that we get passed the first error, but are presented with another error:

(As a side note, in writing this, since I’ve been living in Java-land for the past year, I forgot that the constructor in Python is the __init__() method, and not ROT13(). Having a unit test is a quick way to remind me that I’m doing it wrong.)

In looking at the output, we still need to correct our str() output. Well, let’s correct that real quick.

I know… this is so cheating, but it works! If you run your tests again, all will pass. At some point in your professional life, you will find yourself doing this, whether due to time constraints or to some loose requirement that you don’t quite understand how to implement. This is why you have these tests and multiple versions of these test to make sure what you’re testing works as intended.

Let’s clean it up and make it work with anything, and not just with AAAA. What we’ll do is take each letter in the plaintext, add 13 to the ordinal number of the letter, then convert that ordinal number back into a character using UTC encoding. We can do it in one line.

Running this will also cause our tests to pass. Great!

However, this will only work with letters up to M. What happens when we do something N and beyond?

Well, that’s to be expected. We’ll have to add some code to account for the wraparound after ‘Z’. Let’s introduce a helper method that translates one character at a time, and we’ll have our __str__() method still do a list comprehension and call our helper method. The helper method will simply check to see

Running our tests again, we’ll see that it passes.

Now, let’s try our hand at lower case letters. Our new test will have mixed case, with a good representation from both before and after the m-n midpoint:

Running this will give us interesting results:

It looks like we just need to add to our helper method to do the same thing with lowercase n through z characters.

This will now allow our test to pass.

What about non-alpha characters? We’ll stipulate that non-alpha characters should just pass through unaltered, so that a space in the plaintext is still a space in the ciphertext.

Before our change, we’ll see… not what we wanted.

We’ll put a guard around the block of code in our helper method to keep out all non-alpha characters from being manipulated.

Now, our tests all pass. We have the behavior we want.

This tutorial will end here, but there are many other directions you can go. You can add other methods to continue testing. You can also decide to refactor the helper class to do dictionary lookups instead of integer calculation. Once you change up the helper method, you should be able to run the tests without changing them to confirm that you have the exact same behavior as you did before you do your refactor.

The unittest framework also provides other helpful assertions, like null condition checking and truth validation. It also provides helper methods for setting up and tearing down each test case or each class load.

Remember, the idea of this iterative approach is to first establish your core functionality and slowly add in edge cases as you go, so you’re not overwhelmed with testing every branch of you code all at once. There should be at least one test case per branch.

The complete code for this demo can be found on my Github account. Hope this helps you better understand how to do TDD!

No Comments

You can leave the first : )

Leave a Reply

Your email address will not be published. Required fields are marked *