DEV Community

Cover image for How to Test Python Code using the unittest Module
Sachin
Sachin

Posted on • Originally published at geekpython.in

How to Test Python Code using the unittest Module

You must have written numerous functions and a series of tasks while developing software or a web app. These functions and tasks must work properly. If we encounter errors in the code, debugging becomes difficult.

A good practice would be to divide our code into small units or parts and test them independently to ensure that they work properly.

Python provides a built-in module called unittest that allows us to write and run unit tests.

Getting Started With unittest

The unittest module includes a number of methods and classes for creating and running test cases. Let's look at a simple example where we used the unittest module to create a test case.

# basic.py
import unittest

class TestSample(unittest.TestCase):
    def test_equal(self):
        self.assertEqual(round(3.155), 3.0)

    def test_search(self):
        self.assertIn("G", "Geek")
Enter fullscreen mode Exit fullscreen mode

First, we imported the unittest module, which will enable us to use the classes that will be used to write and execute test cases.

The TestSample class is defined that inherits from the unittest.TestCase which will allow us to use the various assertion methods within our test cases.

We defined two test methods within our TestSample class: test_equal and test_search.

The test method test_equal() tests if round(3.155) is equal to 3.0 using the assertEqual() assertion method.

The test method test_search() tests if the character "G" is present in the string "Geek" using the assertIn() assertion method.

To run these tests, we need to execute the following command in the terminal.

python -m unittest basic.py
Enter fullscreen mode Exit fullscreen mode

This command will launch unittest as a module that searches for and executes the tests in the basic.py file.

Note: The unittest module only discovers and executes those methods that start with test_ or test.

unittest output

By the way, these dots represent a successful test.

We can use the unittest.main() function and put it in the following form at the end of the test script to load and run the tests from the module.

if __name__ == '__main__':
    unittest.main()
Enter fullscreen mode Exit fullscreen mode

This will allow us to run our test file, basic.py in this case, as the main module.

output

More Detailed Result

We can use the -v flag in the terminal or pass an argument verbosity=2 inside the unittest.main() function to get a detailed output of the test.

output using -v flag

Commonly Used Assertion Methods

Here is the list of the most commonly used assertion methods in unit testing.

Method Checks that
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIs(a, b) a is b
assertIsNot(a, b) a is not b
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIn(a, b) a in b
assertNotIn(a, b) a not in b
assertIsInstance(a, b) isinstance(a, b)
assertNotIsInstance(a, b) not isinstance(a, b)

Example

Assume we have some code and want to perform unit testing on it using the unittest module.

# triangle.py
class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

    def perimeter(self, side):
        return self.base + self.height + side
Enter fullscreen mode Exit fullscreen mode

The code defines a class called Triangle which has an init method that initializes the object with the instance variables self.base and self.height.

There are two more methods in the Triangle class: area() and perimeter().

The area() method returns the area of the triangle, which is half the product of the base and height (0.5 * self.base * self.height).

The method parameter() accepts a parameter called side, and because the triangle's parameter is the sum of its three sides, the base and height variables take the place of the other two sides.

Now we can create another Python file in which we'll write some tests and then execute them.

# test_sample.py
from triangle import Triangle
import unittest

class TestTriangle(unittest.TestCase):
    t = Triangle(9, 8)

    def test_area(self):
        self.assertEqual(self.t.area(), 36)

    def test_perimeter(self):
        self.assertEqual(self.t.perimeter(5), 22)

    def test_valid_base(self):
        self.assertGreater(self.t.base, 0)

    def test_valid_height(self):
        self.assertGreater(self.t.height, 0)

if __name__ == '__main__':
    unittest.main(verbosity=2)
Enter fullscreen mode Exit fullscreen mode

The above code imports the Triangle class from the triangle module (triangle.py file) as well as imports the unittest module to write test cases.

The TestTriangle class inherits from the unittest.TestCase which has four test methods. The Triangle class was instantiated with a base of 9 and height of 8 and stored inside the variable t.

The test_area method tests whether self.t.area() is equal to the expected result 36 using the assertEqual() assertion.

The test_perimeter method tests whether self.t.perimeter(5) is equal to 22 using the assertEqual() assertion.

The test_valid_base and test_valid_height methods are defined to test if the base (self.t.base) and height (self.t.height) of the triangle are greater than 0 using the assertGreater() assertion.

The unittest.main(verbosity=2) method retrieves and executes the tests from the TestTriangle class. We'll get a detailed output because we used the verbosity=2 argument.

TestTriangle output

Test for Exception

If you've used assert statements before, you'll know that when one fails, it throws an AssertionError. Similarly, whenever a test method fails, an AssertionError is raised.

We can predetermine the conditions under which our code will generate an error, and then test those conditions to see if they generate errors. This is possible with the assertRaises() method.

The assertRaises() method can be used with context manager so we'll use it in the following form:

def test_method(self):
    with assertRaises(exception_name):
        function_name(argument)
Enter fullscreen mode Exit fullscreen mode

Consider the following function gen_odd(), which generates a series of odd numbers up to the argument n by incrementing the num by 3 and contains only a few checks, where the argument n must be of type int and greater than 0.

# odd.py
def gen_odd(n):
    if type(n) != int:
        raise TypeError("Invalid argument type.")
    if n < 0:
        raise ValueError("Value must be greater than 0.")
    num = 0
    while num <= n:
        if num % 2 == 1:
            print(num)
        num += 3
Enter fullscreen mode Exit fullscreen mode

Now we'll write test methods to simulate conditions that could cause the above code to fail.

from odd import gen_odd
import unittest

class OddTestCase(unittest.TestCase):

    def test_negative_val(self):
        with self.assertRaises(ValueError):
            gen_odd(-5)

    def test_float_val(self):
        with self.assertRaises(TypeError):
            gen_odd(99.9)

    def test_string_val(self):
        with self.assertRaises(TypeError):
            gen_odd('10')

if __name__ == '__main__':
    unittest.main(verbosity=2)
Enter fullscreen mode Exit fullscreen mode

We wrote three test methods in the OddTestCase class to ensure that when invalid arguments are passed, the corresponding error is raised.

The test_negative_val() method asserts that ValueError is raised when gen_odd(-5) is called.

Similarly, the test_float_val() and test_string_val() methods assert that when gen_odd(99.9) and gen_odd('10') are called, respectively, TypeError is raised.

exception checking output

All three tests in the above code passed, which means they all raised corresponding errors, otherwise, the tests would have failed or raised the errors if another exception was raised. Let's put it to the test.

from odd import gen_odd
import unittest

class OddTestCase(unittest.TestCase):

    def test_valid_arg(self):
        with self.assertRaises(TypeError, msg="Valid argument"):
            gen_odd(10)

if __name__ == '__main__':
    unittest.main(verbosity=2)
Enter fullscreen mode Exit fullscreen mode

The above condition within the test_valid_arg() method will not throw a TypeError because gen_odd() function is passed with a valid argument.

OddTestCase output

The above test method failed and raised an AssertionError with the message TypeError not raised : Valid argument.

Skipping Tests

The unittest makes use of the skip() decorator or skipTest() to skip any test method or whole test class on purpose, and we are required to specify the reason why the test is being skipped.

Consider the previous example's code, which we modified by adding the skip() decorator.

from odd import gen_odd
import unittest

class OddTestCase(unittest.TestCase):

    @unittest.skip("Valid argument")
    def test_valid_arg(self):
        with self.assertRaises(TypeError):
            gen_odd(10)

if __name__ == '__main__':
    unittest.main(verbosity=2)
Enter fullscreen mode Exit fullscreen mode

It's clear that the above condition will fail and throw an AssertionError, so we skipped the testing on purpose.

output

What if we wanted to skip the test if a particular condition was true? We can accomplish this by using the skipIf() decorator, which allows us to specify a condition and skip the test if it is true.

from odd import gen_odd
import sys
import unittest

class OddTestCase(unittest.TestCase):

    @unittest.skipIf(sys.getsizeof(gen_odd(10)) > 10, "Exceeded limit")
    def test_memory_use(self):
        self.assertTrue(sys.getsizeof(gen_odd(10)) > 10)
        print(f"Size: {sys.getsizeof(gen_odd(10))} bytes")

if __name__ == '__main__':
    unittest.main(verbosity=2)
Enter fullscreen mode Exit fullscreen mode

The condition in the above skipIf() decorator checks whether the size of gen_odd(10) is greater than 10 bytes, if the condition is true, the test method test_memory_use() is skipped, otherwise, the test is executed.

slipIf output

Expected Failure

If we have a test method or test class with conditions that are expected to be false, we can use the expectedFailure() decorator to mark them as expected failures instead of checking for errors.

from odd import gen_odd
import sys
import unittest

class OddTestCase(unittest.TestCase):

    @unittest.expectedFailure
    def test_memory_use(self):
        self.assertTrue(sys.getsizeof(gen_odd(10)) < 10, msg="Expected to be failed")
        print(f"Size: {sys.getsizeof(gen_odd(10))} bytes")

if __name__ == '__main__':
    unittest.main(verbosity=2)
Enter fullscreen mode Exit fullscreen mode

We've modified the previous code and the condition we are checking inside the test_memory_use() method is expected to be false, which is why the method is decorated with the @unittest.expectedFailure decorator.

expected failure output

Conclusion

We can use the unittest module to write and run tests to ensure that the code is working properly. The test can result in one of three outcomes: OK, FAIL, or Error.

The unittest module provides several assertion methods that are used to validate the code.

Let's recall, what we've learned:

  • the basic usage of unittest module.

  • CLI commands to run the tests.

  • testing if the condition is raising an exception.

  • skipping the tests on purpose and when a certain condition is true.

  • marking a test as an expected failure.


πŸ†Other articles you might be interested in if you liked this one

βœ…How to use assert statements for debugging in Python?

βœ…What are the uses of asterisk(*) in Python?

βœ…What are __init__ and __new__ methods in Python?

βœ…How to implement getitem, setitem and delitem in Python classes?

βœ…How to change the string representation of the object using str and repr methods?

βœ…How to generate temporary files and directories using tempfile in Python?

βœ…Build a custom deep learning model using the transfer learning technique.


That's all for now

Keep coding✌✌

Top comments (0)