Test Driven Development (TDD) with Python

Philosophy:

Having a bug in production is extremely expensive. You probably know that comparison, where a bug found during development is 100 times cheaper than finding the same bug during production. So, we should focus on finding our bugs as soon as possible. Our first line of defensive is testing.

Problem: We can test the whole application Manually. But, it's time consuming.

Solution: We need to have an Automated Testing to solve all the above problems.

Types of Automated Testing:

Unit Tests – It is a piece of a code that invokes another piece of code (unit) and checks if an output of that action is the same as the desired output.

Integration Tests – It is testing a unit without full control over all parties in the test. They are using one or more of its outside dependencies, such as a database, threads, network, time, etc.

Functional Tests – It is testing a slice of a functionality of the system, observing it as a black box and verifying the output of the system to the functional specification.

Acceptance Tests – It is testing does the system fulfill expected business and contract requirements. It is considering the system as a black box.

What is TDD:

 TDD is an evolutionary approach to building and designing software solutions which is consisting of small cycles in which we are writing a unit test, that will initially fail, and then implementing the minimum amount of code to pass that test. After that code can be refactored to follow some good principles.

Image Source: link

It follows Red – Green – Refactor Formula. Which means, we write a test that will fail (Red), Then we implement the code to make the previously written test pass (Green), And finally, we refactor our code.

Another Way of Representation is:

Image Source: link

How is TDD different?

 The crucial difference between TDD and traditional testing is the moment in which we are writing the tests. When we are writing code using TDD we first write the tests and then the code itself, and not another way around.  Another important difference is that we are writing small chunks of code to satisfy our test. This way the process itself drives our design and forces us to keep things simple.

What's the benefit?

 1. The benefit of this approach is that we are minimizing the possibility of forgetting to write tests for some part of the code.

 2. Ideally, we end up with the code that is fully tested upfront and solutions that are implemented using TDD usually have 90%-100% of code covered with tests.


So, by using TDD we avoid creating over complicated designs and overengineered systems. Arguably this is the biggest benefit of this approach.

BEST Python Testing Frameworks

  1. doctest [Another Link]

  2. unittest

  3. pytest

  4. Nose

  5. robotframework with selenium

  6. Testify

Implementation Approach

Sample Code:

import unittest

from rick import Rick


class RickTests(unittest.TestCase):

def test_universe(self):

rick = Rick(111)

self.assertEqual(rick.universe, 111)

if __name__ == '__main__':

unittest.main()

Of course, if we run this test we will get an error saying that Rick class is not existing:

ERROR: test_universe (main.RickTests)

Traceback (most recent call last):

File "rick_tests.py", line 5, in test_universe

rick = Rick(111)

NameError: name 'Rick' is not defined

Ran 1 test in 0.001s

FAILED (errors=1)

We need to define the class and initialize it through the constructor with the value for the universe:

class Rick(object):

def __init__(self, universe):

self.universe = universe

Now, when we re-run the tests, we get this:

test_universe (main.RickTests) … ok

--------------------------------------------------

Ran 1 test in 0.000s

OK

Mock Objects

What are Mock Objects:

 Well, in Object-Oriented programming mock objects are defined as simulated objects. They mimic the behavior of real objects in controlled ways.

Why do we need Mock Objects:

 Well, the whole point of unit testing is isolating certain functionality (unit) and test just that. We are trying to remove all other dependencies.

What are other dependencies: can be a database or file system

  1. we need to take care of the data in the database before and after every test, and that is something we want to avoid.

  2. In some Other times dependency can be just some other class or function.

A mock object should be used in the situations when:

  1. The real object is slow

  2. The real object rarely and is difficult to produce artificially

  3. The real object produces non-deterministic results

  4. The real object does not yet exist (often the case in TDD )


Most useful one with Mock Objects and Mock Classes is this Library.

More Examples on TDD

Example 1

Let’s begin with the usual “hello world”:

import unittest

from mycode import *


class MyFirstTests(unittest.TestCase):


def test_hello(self):

self.assertEqual(hello_world(), 'hello world')

Notice that we are importing helloworld() function from mycode file. In the file mycode.py we will initially just include the code below, which creates the function but doesn’t return anything at this stage:

def hello_world():

pass

Running python mytests.py will generate the following output in the command line:

F

====================================================================

FAIL: test_hello (__main__.MyFirstTests)

--------------------------------------------------------------------

Traceback (most recent call last):

File "mytests.py", line 7, in test_hello

self.assertEqual(hello_world(), 'hello world')

AssertionError: None != 'hello world'

--------------------------------------------------------------------

Ran 1 test in 0.000s

FAILED (failures=1)


This clearly indicates that the test failed, which was expected. Fortunately, we have already written the tests, so we know that it will always be there to check this function, which gives us confidence in spotting potential bugs in the future.

To ensure the code passes, lets change mycode.py to the following:

def hello_world():

return 'hello world'

Running python mytests.py again we get the following output in the command line:

.

--------------------------------------------------------------------

Ran 1 test in 0.000s


OK


Example 2

In the file mytests.py this would be a method test_custom_num_list:

import unittest

from mycode import *


class MyFirstTests(unittest.TestCase):


def test_hello(self):

self.assertEqual(hello_world(), 'hello world')

def test_custom_num_list(self):

self.assertEqual(len(create_num_list(10)), 10)

This would test that the function create_num_list returns a list of length 10. Let’s create function create_num_list in mycode.py:

def hello_world():

return 'hello world'


def create_num_list(length):

pass

Running python mytests.py will generate the following output in the command line:

E.


====================================================================


ERROR: test_custom_num_list (__main__.MyFirstTests)


--------------------------------------------------------------------


Traceback (most recent call last):


File "mytests.py", line 14, in test_custom_num_list


self.assertEqual(len(create_num_list(10)), 10)


TypeError: object of type 'NoneType' has no len()


--------------------------------------------------------------------


Ran 2 tests in 0.000s


FAILED (errors=1)


This is as expected, so let’s go ahead and change function create_num_list in mytest.py in order to pass the test:

def hello_world():

return 'hello world'


def create_num_list(length):

return [x for x in range(length)]

Executing python mytests.py on the command line demonstrates that the second test has also now passed:

..


--------------------------------------------------------------------


Ran 2 tests in 0.000s


OK


For some more examples on how TDD can be implemented, refer here.

Suggested Links:

  1. freecodecamp

  2. rubikscode