Django Series

Lesson 1: Start

Lesson 2: Git

Lesson 3: MVC

Lesson 4: Forms

Lesson 5: Models

Appendix i: Setup

Appendix ii: Shell

Appendix iii: TDD

How to test a function?

 

 

In this article I will show you how to test a function. We will use the Python 3 programming language with the unittest library. You can visit the complete source code here.

 

So let's say we have a function where the requirements are:

Input:

  * is a string

Output:

  * a list of vowels and their frequencies one by one.

  * the total number of consonants in that string.

 

After some though we decide a dictionary would be an ideal output, where keys will be the vowels and the total number of consonants, and the values will be the frequencies. Without further instruction, we will do this in a Python module to provide some isolated environment for our function and tests.

 

Let's create some basic layout for this module. Navigate into a new directory and put in the following command:

touch __init__.py vowelsCheck.py tests.py

 

Base Case

 

So as good TDD practice we write some initial test. Let's test for the base case first, and then we will build from there. What's the base case? Empty string.

 

***tests.py***

import unittest

from vowelsCheck import vowelsCheck

 

 

Class TestVowelFunction(unittest.TestCase):

 

    def test_empty_string(self):

        expected = {}

        result = vowelsCheck('')

        self.assertEqual(result, expected)

 

if __name__ == '__main___':

    unittest.main()

 

As you can see unittest library works this way. You create a class which holds all your tests of one topic, in this case this function. The checking part is done by the methods starts with 'assert'. There are number of them you can use. See more here.

 

Let's write the function:

***vowelsCheck.py***

def vowelsCheck(string):

    pass

 

Run the test:

python3 tests.py

 

Output:

F

 

Let's make it pass by changing our vowelsCheck function:

def vowelsCheck(string):

    return {}

 

Then the test should pass.

 

Normal Conditions

 

We can see that even if a function passes individual tests it doesn't mean it does the what it is required to do. That's why we will test the normal conditions first. Normal conditions are strings of vowels and consonants, upper case and lower case. Let's list all of them and give them as input for our function.

 

***tests.py***

class TestVowelFunction(unittest.TestCase):

 

    def setUp(self):

        self.expected = {}

 

    def test_empty_string(self):

        result = vowelsCheck('')

        self.assertEqual(result, self.expected)

 

    def test_all_consonants(self):

        string = "bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ"

        self.expected = {'consonants': 42}

        result = vowelsCheck(string)

        self.assertEqual(result, self.expected)

 

    def test_all_vowels(self):

        string = "aeiouAEIOU"

        self.expected = {'a': 2, 'e': 2, 'i': 2, 'u': 2, 'o': 2}

        result = vowelsCheck(string)

        self.assertEqual(result, self.expected)

The setUp function will create the test environment and it will run before every test once. Check how self.expected dictionary should look like in each test.

 

Let's run our tests, and it fails as we expect.

FF.

 

Continue with our TDD process. Remember: Create a failing test, make it pass, refactor.

 

Make the test pass by rewriting our function:

***vowelsCheck.py***

from collections import defaultdict

 

 

def vowelsCheck(string):

    vowels = 'aeiou'

    result_dict = defaultdict(int)

 

    for letter in string.lower():

        if letter in vowels:

            result_dict[letter] += 1

        else:

            result_dict['consonants'] += 1

 

    return dict(result_dict)

 

What is defaultdict? It's a dictionary, which does not have KeyError. If you put in a key which doesn't exists, it creates one. Only caveat, you need to specify the type of the values at creation.

 

So the test passes now.

 

Boundary Conditions

 

What if some unexpected input occurs? Let's think about it. There are other type of characters not just vowels and consonants, there are symbols and numbers! If you inspect our function carefully, it is clear that these symbols would be counted as consonants because of the lines:

    else:

        result_dict['consonants'] += 1

 
 

Let's create some more tests to amend this issue:

***tests.py***

class TestVowelFunction(unittest.TestCase):

...

    def test_symbols(self):

        string = "_+-&@$?!)(\/%"

        result = vowelsCheck(string)

        self.assertEqual(result, self.expected)

 

    def test_mixed_chars(self):

        string = "18Mixed_Characters"

        expected = {'a': 2, 'e': 2, 'i': 1, 'consonants': 10}

        result = vowelsCheck(string)

        self.assertEqual(result, expected)

 
Run the tests:

python3 tests.py

 
Failed as expected:

...FF

 
Make it pass, by adjusting our function:

***vowelsCheck.py***

...

def vowelsCheck(string):

    vowels = 'aeiou'

    consonants = 'bcdfghjklmnpqrstvwxyz'

 
    result_dict = defaultdict(int)

 

    for letter in string.lower():

        if letter in vowels:

            result_dict[letter] += 1

        elif letter in consonants:

            result_dict['consonants'] += 1

    return dict(result_dict)

 

The test passes. We are done for boundary conditions.

 

Unexpected Conditions

 
It's time to ask to question what could go wrong? There are number of things, but the most obvious is bad input values. What if our function get a list or an integer as input? It really should raise an error. Remember a function only supposed to do, what it is required to do. Nothing less, nothing more. It shouldn't be able to handle all kinds of inputs. Let's write some tests then:

***tests.py***

class TestVowelFunction(unittest.TestCase):

...

def test_input_not_a_string(self):

    input_list = [1, 3.14, ['item1', 'item2'], (1, 2), b'hello']

        for item in input_list:

            with self.assertRaises(TypeError):

                vowelsCheck(item)

 

What's happening here? We give the function all kinds of inputs and it should really raise TypeError every single time. Let's run it.

…E..

 

Int object, has no attribute, lower. Well, now this bug is expected, but it is a real headache when similar occurs somewhere in thousands of lines of code. Moreover the real problem is not that int has no lower attribute, but that the function shouldn't take int in the first place!

Let's amend this with the following:

***vowelsCheck.py***

...

def vowelsCheck(string):

    if not isinstance(string, str):

        raise TypeError

 

    vowels = 'aeiou'

    consonants = 'bcdfghjklmnpqrstvwxyz'

 

    result_dict = defaultdict(int)

 

    for letter in string.lower():

        if letter in vowels:

            result_dict[letter] += 1

        elif letter in consonants:

            result_dict['consonants'] += 1

    return dict(result_dict)

 

So raise a TypeError when it's not a string. That introduces to a concept called assertive programming. For every function, you should be really clear on what input it shouldn't take and raise an assertion (error) if it did.

 

Run the test. It should now pass.

 
Congratulations! Now, you've seen how to test a function properly. If you can think of any other conditions we should really test, let me know in the comment section.

 

Until next time!

Tags: TDD , tutorial , unit test , beginner