avatarSaverio Mazza

Summarize

Python Testing: Unittest and Alternatives

The unittest framework in Python is a powerful tool for writing automated tests for your code. It follows the XUnit style and borrows its core principles from Java's JUnit framework. Below, I'll delve into some of the core components and functionalities of unittest.

Python Testing: Unittest and Alternatives

Consider the following directory structure

.
├── problems-solving
│   └── data-structures-and-algorithms
│       └── 5-arrays
│           ├── problem-1
│           │   └── solution.py
│           ├── problem-2
│           │   └── solution.py
│           └── problem-3
│               ├── my_solution.py
│               ├── solution.py
│               └── test_helper.py
└── README.md
# problem-3/solution.py
"""
Problem Statement:

Certain applications require arbitrary precision arithmetic. One way to achieve this
is to use arrays to represent integers. Each array entry represents a digit, with the
most significant digit appearing first. A negative leading digit denotes a negative integer.

For example, the sequence [1, 9, 3, 7, 0, 7, 7, 2, 1] represents the number 193707721, and
the sequence [-7, 6, 1, 8, 3, 8, 2, 5, 7, 2, 8, 7] represents the number -761838257287.

The task is to write a program that takes two arrays representing integers and returns
an integer representing their product.

Example:
    If the inputs are [1, 9, 3, 7, 0, 7, 7, 2, 1] and [-7, 6, 1, 8, 3, 8, 2, 5, 7, 2, 8, 7],
    your function should return [-1, 4, 7, 5, 7, 3, 9, 5, 2, 5, 8, 9, 6, 7, 6, 4, 1, 2, 9, 2, 7],
    since 193707721 * -761838257287 = -147573952589676412927.

"""

def solution(num1, num2):
    # Determine the sign of the result
    sign = -1 if (num1[0] < 0) ^ (num2[0] < 0) else 1

    # Take the absolute value of the first digit (which could be negative)
    num1[0], num2[0] = abs(num1[0]), abs(num2[0])

    # Initialize the result list with zeros
    result = [0] * (len(num1) + len(num2))

    # Multiply each digit of num1 with each digit of num2
    for i in reversed(range(len(num1))):
        for j in reversed(range(len(num2))):
            result[i + j + 1] += num1[i] * num2[j]
            result[i + j] += result[i + j + 1] // 10
            result[i + j + 1] %= 10

    # Remove the leading zeroes
    result = result[
        next((i for i, x in enumerate(result) if x != 0), len(result)) :
    ] or [0]

    # Apply the sign to the result
    return [sign * result[0]] + result[1:]
# problem-3/test_helper.py
import unittest
from solution import solution as original_solution

class TestMultiplyArrays(unittest.TestCase):
    def setUp(self):
        self.func = original_solution

    def test_given_example(self):
        arr1 = [1, 9, 3, 7, 0, 7, 7, 2, 1]
        arr2 = [-7, 6, 1, 8, 3, 8, 2, 5, 7, 2, 8, 7]
        result = self.func(arr1, arr2)
        self.assertEqual(
            result, [-1, 4, 7, 5, 7, 3, 9, 5, 2, 5, 8, 9, 6, 7, 6, 4, 1, 2, 9, 2, 7]
        )

    def test_both_positive(self):
        arr1 = [1, 2, 3]
        arr2 = [4, 5, 6]
        result = self.func(arr1, arr2)
        self.assertEqual(result, [5, 6, 0, 8, 8])

    def test_both_negative(self):
        arr1 = [-1, 2, 3]
        arr2 = [-4, 5, 6]
        result = self.func(arr1, arr2)
        self.assertEqual(result, [5, 6, 0, 8, 8])


if __name__ == "__main__":
    unittest.main()

Core Components

  • Test Case: The individual test in unittest is represented by a method within a subclass of unittest.TestCase. Within TestMultiplyArrays, each method that starts with the word test_ is an individual test case. These methods represent specific scenarios to test. For instance, test_given_example tests if the function works as expected with a given example, and test_both_positive tests the function when both input arrays have positive numbers.
  • Test Suite: A test suite is a collection of test cases, test suites, or both. It is used to aggregate tests that should be executed together. In this example, although it’s not explicitly defined as a test suite, the TestMultiplyArrays class effectively serves as a mini test suite. It aggregates three test cases (test_given_example, test_both_positive, and test_both_negative) that are all related to testing the functionality of the original_solution function. In more complex projects, you might want to explicitly create a test suite that groups multiple test cases or even other test suites. This can be done using unittest.TestSuite.
  • Test Runner: The test runner is responsible for running the test suite and providing the results. In your example, the test runner is invoked when you run the script, which calls unittest.main().When you run this script, the unittest framework automatically recognizes that TestMultiplyArrays is a subclass of unittest.TestCase and runs all the individual test methods in the class. The test runner then provides a textual summary of which tests passed and which failed.

Key Methods

  • setUp(): The setUp() method is called before each test method is run. This is useful for setting up any preconditions for your tests. In your example, it initializes self.func to original_solution. This way, you can use self.func in each test method without having to initialize it repeatedly.
  • tearDown(): Called after each test method to clean up the test environment.

Additional Tools

In the future, we will test the following tools:

  • pytest: A popular alternative to Python’s built-in unittest framework.
  • nose2: Successor to the now-unmaintained nose, offers many features like test attributes, layers, and plugins.
  • tox: For testing your code under multiple Python environments.

pytest

Strengths

  • Simplicity: Writing tests with pytest requires less boilerplate code. You don’t have to write class-based tests if you don’t want to.
  • Rich Plugin Architecture: Extensive set of plugins and the ability to create your own.
  • Powerful and Flexible: Advanced features like fixtures for setup code, along with built-in support for parallel test execution.
  • Detailed Info on Failures: Provides a detailed report that makes it easier to debug why a particular test failed.

Weaknesses

  • Learning Curve: While it’s easy to get started, mastering advanced features and plugins can take time.
  • Too Many Features: The sheer number of features can be overwhelming for simple projects.

nose2

Strengths

  • Extensibility: Designed to be extended more easily than unittest, and has a simpler internal structure.
  • Test Discovery: Automatically discovers all test cases.
  • Plugins: Inherits a variety of plugins from its predecessor, nose, and you can also write your own.

Weaknesses

  • Less Popular: Not as widely adopted as unittest or pytest, so community support is limited.
  • Less Feature-Rich: While it does offer some features not present in unittest, it lacks the feature richness of pytest.

tox

Strengths

  • Multi-Environment Testing: Allows you to test your code under multiple Python environments, ensuring compatibility.
  • Automation: Easily configured to run a battery of tests, making it ideal for integration with CI/CD pipelines.
  • Isolation: Tests are run in isolated environments, ensuring that external factors do not affect your tests.

Weaknesses

  • Complexity: Setting up multiple environments can be time-consuming and complex.
  • Resource-Intensive: Running tests in multiple environments consumes more resources.

Applicability to Your Example

  • pytest: Would make the tests simpler and easier to write, especially if you plan to extend them with more complex scenarios.
  • nose2: Could be a good fit if you are looking for a middle-ground between unittest and pytest in terms of features and complexity.
  • tox: Useful if you want to ensure that your code works across multiple Python environments, but may be overkill for a small, single-purpose script.

Subscribe to my newsletter to get access to all the content I’ll be publishing in the future.

Python
Testing
Software Engineering
Unittest
Pytest
Recommended from ReadMedium