Test Driven Development with Python – Part 2

In the previous tutorial, we introduced you to a little test driven development with Python (TDD). Welcome! to the second part of our Python TDD tutorial. Here we are going to continue the development of propers library.

Python TDD Some More

Make sure that you read the previous article: Python 3 Testing & TDD – Part 01

def test_value_split(self):
    prop = Property("product.version = 1.2.3")
    self.assertEqual(prop.value, "1.2.3")

Boom! Wrote a failing test case. Now we are going to make it pass.

class Property(object):
    def __init__(self, line):
        self.line = line
        self.key = line.split("=")[0].strip()
        self.value = "1.2.3"

Awesome. Everything is passing. Let’s add another test case.

def test_value_split_another(self):
    prop = Property("some=key")
    self.assertEqual(prop.value, "key")

For this test case to pass we need to add some special logic. (Since we obviously hard coded the previous one)

class Property(object):
    def __init__(self, line):
        self.line = line

        key_value = line.split("=")

        self.key = key_value[0].strip()
        self.value = key_value[1].strip()

Looks good to me. Now, we can refactor the test cases. Let’s introduce a for loop.

def test_value_split(self):
    tests = (
        ("product.version = 1.2.3", "1.2.3"),
        ("some=key", "key")
    )
    for line, expected in tests:
        prop = Property(line)
        self.assertEqual(
            prop.value, expected,
            "{0}|prop.value={1}"
                .format(repr(line), repr(expected)))

Data-Driven Python 3 Testing with Parameterized

Ah! That looks good. Oh wait I see duplicated code. Yikes! That’s nasty. There is a neat little package called parameterized that we can use for creating data-driven test cases. Let’s use it.

Python 3 TDD Data Driven Parameterized
Python 3 TDD Data Driven Parameterized
Time to do some test driven development with Python using parameterized.

$ pip install parameterized

Now that we have installed the module we can simply use it.

import unittest
from parameterized import parameterized

from propers.properties import Property


class PropertiesTestCase(unittest.TestCase):
    def test_property_creation(self):
        prop = Property("a=b")
        self.assertTrue(prop is not None)

    @parameterized.expand([
        ("a=b", "a", "b"),
        ("x = y", "x", "y"),
        ("product.version = 1.2.3", "product.version", "1.2.3"),
        ("some=key", "some", "key")
    ])
    def test_property_split(self, line, key, value):
        prop = Property(line)
        message = "input={0}|prop.key={1}|" \
                  "prop.value={2}".format(repr(line), key, value)
        self.assertEqual(prop.key, key, message)
        self.assertEqual(prop.value, value, message)

That looks good. We can simply add more test cases easily. Let’s add the following test cases. Understand that you should write simplest possible test cases and only one test case at a time.

If a test case you add passes automatically, make sure it is actually working.

After that you can add an extra test case or two.

("no.value=", "no.value", ""),
("no.value", "no.value", "")

Ah seems like one of them passes but one of them fails. Let’s fix that.

class Property(object):
    def __init__(self, line):
        self.line = line

        key_value = line.split("=")

        self.key = key_value[0].strip()
        self.value = ""
        if len(key_value) > 1:
            self.value = key_value[1].strip()

More Data Driven Tests for Test Driven Development with Python

Let’s introduce following test cases.

("v==1", "v", "=1"),
("v=#1", "v", "#1"),
("v#=#1", "v#", "#1"),
("v#==#1", "v#", "=#1"),

This is not directly in the requirements, but I think we need to add it. We simply need to specify that there is only one max-split.

key_value = line.split("=", 1)

Now, that is in order. Let’s write a failing test again.

@parameterized.expand([
    ("=b",),
    ("",)
])
def test_property_split_errors(self, line):
    with self.assertRaises(PropertyParserException):
        _ = Property(line)

Afterwards, I’m going to raise an error if there is an issue with the key.

class Property(object):
    def __init__(self, line):
        self.line = line

        key_value = line.split("=", 1)
        self.key = key_value[0].strip()

        if not self.key:
            raise PropertyParserException()

        self.value = ""
        if len(key_value) > 1:
            self.value = key_value[1].strip()


class PropertyParserException(Exception):
    pass

Test Driven Development with Python : It’s Hammer Time (Time to Refactor)

Since above test cases are passing, we can refactor the code base. Even if many programmers say that you cannot. You can do better. You can always make code better with Python TDD..

class Property(object):
    def __init__(self, line):
        self.line = line
        key_value = line.split("=", 1)
        self._extract_key(key_value)
        self._extract_value(key_value)

    def _extract_key(self, key_value):
        self.key = key_value[0].strip()
        if not self.key:
            raise PropertyParserException()

    def _extract_value(self, key_value):
        self.value = ""
        if len(key_value) > 1:
            self.value = key_value[1].strip()

This looks neat. I’ve also refactored PropertiesTestCase to PropertyTestCase to make things more meaningful.

class PropertyTestCase(unittest.TestCase):
    def test_property_creation(self):
        prop = Property("a=b")
        self.assertTrue(prop is not None)

    @parameterized.expand([.....

I’ve also renamed the file accordingly.

Propers – Comments Python TDD Style

Now that you know quite a bit about test driven development with Python,  let’s see if there are any more objects that we need for out project.

import unittest

from parameterized import parameterized


class CommentTestCase(unittest.TestCase):
    @parameterized.expand([
        ("# Some comment",),
    ])
    def test_parse_comment(self, line):
        comment = Comment(line)

Viola! Yet another failing test case. Time to make it pass.

class Comment(object):
    def __init__(self, line):
        pass

Now to update the test.

class CommentTestCase(unittest.TestCase):
    @parameterized.expand([
        ("# Some comment", " Some comment"),
    ])
    def test_parse_comment(self, line, data):
        comment = Comment(line)
        self.assertEqual(comment.data, data)

Parser should be able to access comment data. Therefore, we need to extract comments this way.

class Comment(object):
    def __init__(self, line):
        self.line = line
        self.data = self.line[self.line.index("#") + 1:]

Alright, we have made the test pass. Let’s add more test cases.

("  # Some comment", " Some comment"),
("#### Some comment", "### Some comment"),

We can simply make it pass by calling strip on input line.

self.line = line.strip()

Let’s add yet another test case. I want to ensure that the spaces at the end of the comments are preserved.

(" #### Some comment ", "### Some comment "),

Simply change the line assignment to use lstrip will do the trick.

self.line = line.lstrip()

Now we need to ensure that there are no unwanted data being injected to the Comment. Let’s add more tests to validate.

@parameterized.expand([
    ("          ",),
    ("",)
])
def test_parse_comment_errors(self, line):
    with self.assertRaises(CommentParserException):
        _ = Comment(line)

Let’s make the test pass by doing the following change.

class Comment(object):
    def __init__(self, line):
        self.line = line.lstrip()

        if not self.line:
            raise CommentParserException()

        self.data = self.line[self.line.index("#") + 1:]


class CommentParserException(Exception):
    pass

Python TDD : This Is It for Part 02

I’ll be coming up with Part 03 in the near future. Previous article: Python 3 Testing & TDD – Part 01

Did you like this article? Go ahead and check out our other programming articles! We have tutorials on PythonJavaJavaScript and cool tips and tricks that can help you get stuff done.

Like what you see? Subscribe to our email list and hit us with a like on our Facebook page to get the latest news updates and tutorials straight to your newsfeed!

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.