Parameterized Unit Testing in Python

Abid Ullah Oct 10, 2023
  1. Purpose of Parameterized Unit Testing in Python
  2. Examples of Parameterized Unit Testing in Python
  3. Benefits of Parameterized Unit Testing in Python
  4. Python Libraries That Support Parameterized Unit Testing
Parameterized Unit Testing in Python

Unit testing is one powerful tool to maintain the software’s quality. Unit tests on software are a series of checks on a piece of code to ensure that the software is developed in line with the software design specifications and behaves or responds as intended for the end-user.

In Python, we can generate one test for each item or test case on the go using a parameterized unit test. This article will explore Python’s parameterized unit tests.

Purpose of Parameterized Unit Testing in Python

When developers write unit tests, they often adopt the one test for one case approach. The most similar or related tests are then combined into a suite.

Look at the suite of tests in the code below:

class TestSuite(TestCase):
    def test_first_case(self):
        assert example_test()

    def test_second_case(self):
        assert more_example_tests()

    def test_third_case(self):
        assert another_example_test()

Testing through suites like these would mean that each test case must undergo a series of custom stages of preparation, execution, and, finally, assertion. If each of these test cases is similar, the resulting suite would be a mess of duplicate or redundant code, which is nothing short of a curse in the world of software developers.

These suites also result in large code bases that are extremely hard to maintain when changes are made (which is to be expected). To counter this issue, we have the blessing of dynamic or parameterized unit testing in Python, allowing us to generate a singular test for multiple cases.

Examples of Parameterized Unit Testing in Python

Let’s dive into an example test scenario or method that we can use as a base to understand parameterized unit testing in Python and how it makes our lives as software developers much easier.

Below is a simple function that we want to test:

def compute(a, b):
    return (a + b) / (a * b)

Now, like any test, we know that there are multiple inputs that we need to apply to the function above to test that it behaves in the desired manner. To specify the test parameters more elaborately, we need to test the method against parameters that are:

  • positive integers
  • negative integers
  • long integers
  • regular integers
  • float number
  • either or both numbers are zero
  • either or both arguments are undefined
  • either or both arguments are strings or other kinds of objects

If this were to be tested using the one test per case method, we’d be writing a long series of repetitive code for a single-line method. You can imagine how hectic and tedious this can be for methods based on multiple lines of code involving loops and conditions.

With parameterization, we need only do this:

def test_compute(self, a, b, expected, raises=None):
    if raises is not None:
        with self.assertRaises(raises):
            compute(a, b)
        assert compute(a, b) == expected

With this, we would need to prepare a list of parameters that best test all the possibilities for the compute method. The list of parameters can be as long as we want or just a collection of the case we need to be tested.

Here is an example of a list of parameters that can be passed into the unit test above.

The output of the code:

a           | b   | expected | raises
-2          | 2   | 0        |
0           | 2   |          | ZeroDivisionError
2           | 2   | 1        |
0.5         | 0.4 | 4.5      |
None        | 2   |          | TypeError
"10"        | "1" |          | TypeError
3000000000L | 2   | 0.5      |

As can be seen, this list and the parameterized unit test above make it much more efficient for us to test out the various scenarios of arguments that can be passed into the compute() method. It also makes recording the varying results much easier.

Benefits of Parameterized Unit Testing in Python

Writing regular unit tests can be highly repetitive, exhaustive, time-consuming, and nerve-racking, especially when we want the tests to walk through each software scenario and ensure everything is up to the mark.

Using parameterized unit testing in Python means that we, as developers, don’t have to worry about writing hundreds of code lines to test the various scenarios that may come up in the software being developed to maintain its quality. All we require is a single test to test all scenarios that need to be tested.

This means that the test code base will always be easy to maintain with little to no repetition, resulting in efficient test cases and time-saving results.

Furthermore, parameterized unit tests force the developer in question to think more clearly and definitively about all the possible inputs going into the method. It helps keep track of edge cases like the large integers or inconsistent input types for our example above and any erroneous cases such as the undefined inputs.

This test also naturally forces the developer to think more about how the edge cases can be handled instead of just thinking about the kind of input that may break their code. It helps maintain the quality of the code by forcing us to predict what the function is expected to do under a particular combination of circumstances and leads to the development of conceptual and efficient solutions to any problems faced.

To add to it all, if we decide to use a more exhaustive or extensive list of parameters to test our methods, we may end up having results and solutions for test cases that we first thought to be too trivial to write a test case for. However, with a parameterized test at our disposal, we only need to enter the combination we want tested to see how the code responds.

Python Libraries That Support Parameterized Unit Testing

Python offers to make our life easier with the parameterized library, which makes developing the test method straightforward. All we have to do is import parameterized and pass the parameters that we want to be tested through the parameterized decorator in our method in the test suite like so:

from parameterized import parameterized

class TestSuite(TestCase):
    @parameterized.expand([("test_1", 0, 0, 0), ("test_2", 0, 1, 1)])
    def test_my_feature(self, name, in_1, in_2, expected):
        assert my_feature(in_1, in_2) == expected

Another way to do this is to create a .csv file with a list of all the parameters. Then, we pass it to the decorator to test like so:

def load_test_cases():
    return load_from_csv("my_feature_parameters.csv")

class TestSuite(TestCase):
    def test_my_feature(self, name, in_1, in_2, expected):
        assert my_feature(in_1, in_2) == expected

We have covered everything from why we need to use dynamic (parameterized) unit testing to the different ways we can implement it and its benefits in the software development lifecycle. We hope you find this article helpful in understanding how to generate unit testing in Python.

Author: Abid Ullah
Abid Ullah avatar Abid Ullah avatar

My name is Abid Ullah, and I am a software engineer. I love writing articles on programming, and my favorite topics are Python, PHP, JavaScript, and Linux. I tend to provide solutions to people in programming problems through my articles. I believe that I can bring a lot to you with my skills, experience, and qualification in technical writing.


Related Article - Python Unit Test