Skip to content

Instantly share code, notes, and snippets.

@Tehnix
Created August 5, 2013 23:36
Show Gist options
  • Save Tehnix/6160640 to your computer and use it in GitHub Desktop.
Save Tehnix/6160640 to your computer and use it in GitHub Desktop.
A very shallow look at unit testing in python.

With TDD (test driven development) having so much traction in our day and age, I thought it would be highly relevant to explore the concept of unit testing and such.

The idea behind test driven development, is that you first write your tests for your code, and then you write the code. This way, you can constantly watch if the code passes the tests, meaning the implementation is complete. This also forces you to think about your code before you write it, often helping you in visualizing what needs to be done and how.

A little note though, I have limited experience writing actual tests, but do know a bit about the idea/concept behind TDD.

Python gives us the lovely unittest module, which makes unit testing easy. This is, in reality, all you need to get started, but we're gonna get nose, which extends unittest to make testing easier (as is said on their site).

You can install nose via easy_install:

easy_install nose

or pip:

pip install nose

###Set up For the purpose of future use, the examples I'll show will be using a folder structure that would likely resemble a real-world project folder structure.

runtests.py
src/
---- __init__.py
---- calculator.py
tests/
---- __init__.py
---- test_calculator.py

The calculator.py is going to be a little class that does some basic calculations. These are easy test cases. Let's start by laying out the foundation of our Calculator class.

"""
Simple class to perform some basic arithmetic

"""

class Calculator(object):
    """Perform basic arithmetic"""
    
    def __init__(self):
        pass
    
    def add(self, *nums):
        pass
    
    def multiply(self, *nums):
        pass
        
    def divide(self, *nums):
        pass

And then we quickly set up our tests/test_calculator.py file:

"""
Testing src/calculator.py

"""

import unittest
import src.calculator

class CalculatorTest(unittest.TestCase):
    """Tests performed on the Calculator class"""
    
    def setUp(self):
        """Set up our test"""
        self.calc = src.calculator.Calculator()
    
    def test_add_adds_all_numbers(self):
        pass
        
    def test_multiply_multiplies_all_numbers(self):
        pass
    
    def test_divide_divides_all_numbers(self):
        pass

Now, let me explain what we did here. First we import the unittest module, then (by convention) we name our test class with the same name as the class we're testing with the added Test behind the name.

In the CalculatorTest class, we inherit from unittest.TestCase, so we'll get all the power from the TestCase class. There are two methods to take notice of, the first is the setUp method, which allows you to do something before any test is run. The other is tearDown (which I haven't used here), which allows you to remove or clean up whatever needs cleaning up before the test finishes.

Here, I use the setUp method to instantiate the class I'm testing and save it in an appropriate instance variable self.calc. Another thing to note down, is that every method starting with test, will be run automatically by the unittest module.

###Running tests Now, to run our tests, we're gonna make a quick little script called runtests.py, in our project root.

"""
Run all starting with test

"""

import sys
import os
import nose

def main():
    sys.path.insert(0, os.path.dirname(__file__))
    nose.main()
    sys.exit(0)

if __name__ == '__main__':
    main()

You then just run this by doing python runtests.py. You should see something like this:

....
----------------------------------------------------------------------
Ran 3 tests in 0.006s

OK

Which indicates that the tests passed (which they should since we haven't implemented them yet).

Now, lets get some code in our tests, we start by implementing the test for the addition method.

def test_add_adds_all_numbers(self):
     self.assertEqual(self.calc.add(1, 1, 1, 1), 4)

We then try and run our test, and it spits out something like the following:

F...
======================================================================
FAIL: test_addition_adds_all_numbers (tests.test_calculator.CalculatorTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/Tenshi/.testing/tests/test_calculator.py", line 17, in test_addition_adds_all_numbers
    self.assertEqual(self.calc.add(1, 1, 1, 1), 4)
AssertionError: None != 4

----------------------------------------------------------------------
Ran 3 tests in 0.008s

FAILED (failures=1)

Which is expected since the assertion wasn't correct.

Now, in the spirit of TDD, we try and implement the feature in our add method, so the test will turn out successfull. We add the following in our Calculator class:

    ...
    
    def add(self, *nums):
        """Add all numbers in *nums sequentially"""
        total = 0
        for i in nums:
        total += i
        return total
    
    ...

Run the test again, and raise our arms in success! \o/

....
----------------------------------------------------------------------
Ran 3 tests in 0.006s

OK

We can add a few more assertions to our test to make sure it isn't a false positive. Also, a quick note: the assertEquals is one of the many methods that we inherit from the unittest.TestCase class, you can find a list of the avaible methods in the documentation of the class.

    ...
    def test_add_adds_all_numbers(self):
        self.assertEqual(self.calc.add(9), 9)
        self.assertEqual(self.calc.add(972, 13, 1312), 2297)
        self.assertEqual(self.calc.add(100, 2, 99, 1), 202)
        self.assertEqual(self.calc.add(11, 87, 29, 18, 19, 242, 7), 413)
    ...

We do this the same way for the other methods, since they are basically the same.

Our Calculator class:

"""
Simple class to perform some basic arithmetic

"""

from __future__ import division

class Calculator(object):
    """Perform basic arithmetic"""
    
    def __init__(self):
        pass
    
    def add(self, *nums):
        """Add all numbers in *nums sequentially"""
        total = 0
        for i in nums:
            total += i
        return total
    
    def multiply(self, *nums):
        """Multiply all numbers in *nums sequentially"""
        if len(nums) == 0:
            return 0
        total = 1
        for i in nums:
            total *= i
        return total
        
        
    def divide(self, *nums):
        """Divide 1 by all numbers in *nums sequentially"""
        if len(nums) == 0:
            return 0
        total = 1
        for i in nums:
            total /= i
        return total

And our CalculatorTest class:

"""
Testing src/calculator.py

"""

import unittest
import src.calculator

class CalculatorTest(unittest.TestCase):
    """Tests performed on the Calculator class"""
    
    def setUp(self):
        """Set up our test"""
        self.calc = src.calculator.Calculator()
    
    def test_add_adds_all_numbers(self):
        self.assertEqual(self.calc.add(9), 9)
        self.assertEqual(self.calc.add(972, 13, 1312), 2297)
        self.assertEqual(self.calc.add(100, 2, 99, 1), 202)
        self.assertEqual(self.calc.add(11, 87, 29, 18, 19, 242, 7), 413)
        
    def test_multiply_multiplies_all_numbers(self):
        self.assertEqual(self.calc.multiply(9), 9)
        self.assertEqual(self.calc.multiply(972, 13, 1312), 16578432)
        self.assertEqual(self.calc.multiply(100, 2, 99, 1), 19800)
        self.assertEqual(self.calc.multiply(11, 87, 29, 18, 19, 242, 7), 16078645044)
    
    def test_divide_divides_all_numbers(self):
        self.assertEqual(self.calc.divide(9), 0.1111111111111111)
        self.assertEqual(self.calc.divide(972, 13, 1312), 6.031933538708607e-08)
        self.assertEqual(self.calc.divide(100, 2, 99, 1), 5.0505050505050505e-05)
        self.assertEqual(self.calc.divide(11, 87, 29, 18, 19, 242, 7), 6.2194295431204e-11)

Running our tests again, and it should yield:

...
----------------------------------------------------------------------
Ran 3 tests in 0.006s

OK

###A bit further This is all fine and dandy if you're always returning something in your methods/functions, but what if you do OOP, and need to check the state of your instance (ie, check if an instance variable has changed to what it should). Well, that's not hard at all actually.

If we go by the normal OOP way of making setters and getters (or, you can just access the attribute directly), we could save the last returned result in the Calculator class to an instance variable, and then check if that variable is updated and updated correctly.

But first, we write the test for it, like so:

class CalculatorTest(unittest.TestCase):
    ...
    def test_that_result_is_set_to_total_after_an_operation(self):
        self.assertEqual(self.calc.result(), 0)
        self.calc.divide(9)
        self.assertEqual(self.calc.result(), 0.1111111111111111)
        self.calc.add(972, 13, 1312)
        self.assertEqual(self.calc.result(), 2297)
        self.calc.multiply(11, 87, 29, 18, 19, 242, 7)
        self.assertEqual(self.calc.result(), 16078645044)
    ...

Currently this will fail our test since we haven't altered our Calculator class yet.

To implement this in our Calculator class, we do it thusly:

class Calculator(object):
    """Perform basic arithmetic"""
    
    
    def __init__(self):
        self._result = 0
    
    def setResult(self, result):
        self._result = result
        
    def result(self):
        return self._result
    
    def add(self, *nums):
        """Add all numbers in *nums sequentially"""
        total = 0
        for i in nums:
            total += i
        self.setResult(total)
        return total
    
    def multiply(self, *nums):
        """Multiply all numbers in *nums sequentially"""
        if len(nums) == 0:
            return 0
        total = 1
        for i in nums:
            total *= i
        self.setResult(total)
        return total
        
        
    def divide(self, *nums):
        """Divide all numbers in *nums sequentially"""
        if len(nums) == 0:
            return 0
        total = 1
        for i in nums:
            total /= i
        self.setResult(total)
        return total

The setResult is the setter for the instance attribute _result, and result is the getter. We then also make sure we store the total in the arithmetic methods, before returning. Now the tests should happily run through and return an OK.

###Sum up We learned a bit about TDD (Test Driven Development), we learned that python has its own unit testing module, and we learned how to structure and write tests for our class'. This should give you a starting point in TDD and should allow you to easily expand from here.

Later on I will go more in depth with unit testing and functional testing.

@Mcgyner
Copy link

Mcgyner commented Jul 9, 2021

class Calculator(object):

def __init__(self):
    self._result = 0

def setResult(self, result):
    self._result = result
    
def result(self):
    return self._result

def add(self, *nums):
    """Add all numbers in *nums sequentially"""
    total = 0
    for i in nums:
        total += i
    self.setResult(total)
    return total

def multiply(self, *nums):
    """Multiply all numbers in *nums sequentially"""
    if len(nums) == 0:
        return 0
    total = 1
    for i in nums:
        total *= i
    self.setResult(total)
    return total
    
    
def divide(self, *nums):
    """Divide all numbers in *nums sequentially"""
    if len(nums) == 0:
        return 0
    total = 1
    for i in nums:
        total /= i
    self.setResult(total)
    return total

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment