Python

How to Get Started with Unit Testing in Python

The accuracy and dependability of the code is ensured through unit testing which is a crucial component of software development. Software engineers or developers build and execute these tests which are typically automated to make sure that a “unit” or component of an application complies with design requirements and performs as intended. Since they thoroughly understand the code’s design and implementation, the developers who wrote the code that is being tested frequently write the unit tests.

How to Work with Unit Testing in Python

To get started with unit testing in Python, we can follow these steps:

    1. Import either the “unittest” module or use a third-party library such as pytest, but using the standard library is recommended.
    2. Create a “test” class that is inherited from unittest.TestCase.
    3. Write the test methods in the class. The prefix of the methods is “test_”.
    4. Check the expected behavior of the code.
    5. Run the unit tests by calling the unittest.main() function.

In this article, we will provide the step by step procedure to write and test the unit testing in Python.

Step1: Setup the Project Directory Structure

Make the project’s directory structure first. For instance:

Calculator_Project
├──MyCalc.py
└── Unit_Tests/
    ├──CalcTest.py

 
In this project structure, “MyCalc.py” contains the code that we want to test using the unit testing that is created in the “tests” folder.

Step 2: Write the Code to be Tested

Vending Machine Class:

Writing the test cases for different aspects of the “VendingMachine” class is crucial to ensure its correctness. Let’s expand on the example by adding more test cases for various functionalities of the vending machine which may include adding the products, checking the inventory, and returning the change.

In the following code, the class represents a vending machine that can store and dispense the products. The class’s __init__() method initializes the vending machine’s inventory and balance. The inventory is a dictionary that maps the product names to the product information, including the price and quantity. The balance is the amount of money that the vending machine has collected.

The add_product() method of the class adds a product to the vending machine’s inventory. If the product is already in the inventory, the quantity is increased. Otherwise, a new entry is added to the inventory.

The class’s dispense_product() method dispenses a product from the vending machine. The method requires the product’s name and payment amount as two parameters. The procedure initially verifies if the item exists in the vending machine’s stock. If so, this method verifies that at least one of the products is in stock and that the amount paid is larger than or equal to the product’s price. If both requirements are satisfied, the procedure reduces the product’s inventory by one, raises the vending machine’s balance by the product’s price, and gives the customer their change. If not, the procedure returns “None”.

class VendingMachine:
    def __init__(self):
        self.inventory = {}
        self.vending_balance = 0
    def check_inventory(self):
        inventory_quantities = {product_name: product_info['product_quantity'] for product_name, product_info in self.inventory.items()}
        return inventory_quantities
    def add_new_product(self, product_name, product_price, product_quantity):
        if product_name in self.inventory:
            self.inventory[product_name]['product_quantity'] += product_quantity
        else:
            self.inventory[product_name] = {'product_price': product_price, 'product_quantity': product_quantity}
        print("Product added successfully!")
    def dispense_product(self, product_name, dispense_amount_paid):
        if product_name in self.inventory:
            if dispense_amount_paid >= self.inventory[product_name]['product_price'] and self.inventory[product_name]['product_quantity'] > 0:
                self.inventory[product_name]['product_quantity'] -= 1
                difference = dispense_amount_paid - self.inventory[product_name]['product_price']
                self.vending_balance += self.inventory[product_name]['product_price']  # Corrected balance update
                return difference
            else:
                print("Insufficient quantity!")
                return None
        else:
            print("Product does not exists")
            return None

 

Step 3: Write the Unit Tests

Create a test file now – “VendingUnitTests.py” for example. The “unit test” class now tests the “VendingMachine” class’s following methods:

    • check_inventory()
    • add_new_product()
    • dispense_product()

The “unit test” classes are descended from the “unittest.TestCase” class. This class provides a variety of methods that can be used to check that the code is operating as intended.

The “setUp()” function initiates each unit test by creating a new instance of the “VendingMachine” class.

When the inventory is empty, the test_check_inventory_with_empty_inventory() test case makes sure that the check_inventory() method returns an empty dictionary.

The check_inventory() method produces a dictionary with the right product and amount when there is only one item in the inventory, according to the test_check_inventory_with_one_product() test case.

The add_new_product() method properly updates the inventory when a new product is added to an existing inventory, according to the test_add_new_product_with_existing_product() test case.

The add_new_product() method successfully updates the stock level when a new product is added to an empty inventory, as shown by the test_add_new_product_with_new_product() test case.

The test_dispense_product_with_sufficient_funds_and_quantity() test case verifies that the dispense_product() method correctly dispenses a product when the customer makes a sufficient payment and there is enough amount of the product in stock.

The test_dispense_product_with_non_existent_product() test case confirms that the dispense_product() method returns “None” when a client tries to dispense a product that is not in the inventory.

When a consumer attempts to dispense a product but there is not enough in the inventory, the test_dispense_product_with_insufficient_quantity() test case validates that the dispense_product() method returns “None”.

When the file is directly executed, Python is instructed to run the unit tests via the “__main__” block after the code.

import unittest
from VendingMachine import VendingMachine as VM
class TestVendingMachine(unittest.TestCase):
    def setUp(self):
        self.vending_machine = VM()
    def test_check_inventory_with_empty_inventory(self):
        expected_inventory = {}
        actual_inventory = self.vending_machine.check_inventory()
        self.assertDictEqual(expected_inventory, actual_inventory)
    def test_check_inventory_with_one_product(self):
        self.vending_machine.add_new_product("Cola", 1.5, 10)
        expected_inventory = {"Cola": 10}
        actual_inventory = self.vending_machine.check_inventory()
        self.assertDictEqual(expected_inventory, actual_inventory)

    def test_add_new_product_with_existing_product(self):
        self.vending_machine.add_new_product("Cola", 1.5, 10)
        self.vending_machine.add_new_product("Cola", 1.5, 5)
        expected_inventory = {"Cola": 15}
        actual_inventory = self.vending_machine.check_inventory()
        self.assertDictEqual(expected_inventory, actual_inventory)
    def test_add_new_product_with_new_product(self):
        self.vending_machine.add_new_product("Sprite", 1.25, 5)
        expected_inventory = {"Sprite": 5}
        actual_inventory = self.vending_machine.check_inventory()
        self.assertDictEqual(expected_inventory, actual_inventory)
    def test_dispense_product_with_sufficient_funds_and_quantity(self):
        self.vending_machine.add_new_product("Cola", 1.5, 10)
        dispense_amount_paid = 2.0
        product_name = "Cola"
        expected_change = 0.5
        actual_change = self.vending_machine.dispense_product(product_name, dispense_amount_paid)
        self.assertEqual(expected_change, actual_change)
        self.assertEqual(9, self.vending_machine.check_inventory()[product_name])

    def test_dispense_product_with_non_existent_product(self):
        dispense_amount_paid = 2.0
        product_name = "Sprite"
        actual_change = self.vending_machine.dispense_product(product_name, dispense_amount_paid)
        self.assertIsNone(actual_change)
    def test_dispense_product_with_insufficient_quantity(self):
        self.vending_machine.add_new_product("Cola", 1.5, 1)
        dispense_amount_paid = 1
        product_name = "Cola"
        actual_change = self.vending_machine.dispense_product(product_name, dispense_amount_paid)
        self.assertIsNone(actual_change)
        self.assertEqual(1, self.vending_machine.check_inventory()[product_name])
if __name__ == '__main__':
    unittest.main()

 

Step 4: Run the Tests

Open a terminal, go to the working project directory, and enter the following command to execute the unit tests:

python VendingUnitTests.py

 

Step 5: Analyze the Results


Here is another example:

With the __init__ method to setup a patient data (name, age, and blood pressure) and a get_blood_pressure_category function to classify the patient’s blood pressure into several health categories, this code creates a Python class called “Patient”.

The get_blood_pressure_category method divides the blood pressure value into the systolic and diastolic pressures, transforms them into integers, and then compares these values to predetermined thresholds. It returns a corresponding blood pressure category such as “Normal”, “Elevated”, “Stage 1 Hypertension”, or “Stage 2 Hypertension” if the systolic and diastolic pressures fall within specific ranges. The code’s objective is to evaluate and categorize a patient’s blood pressure per accepted medical standards.

Patient.py:

class Patient:
def __init__(self, name, age, blood_pressure):
    self.name = name
    self.age = age
    self.blood_pressure = blood_pressure
def get_blood_pressure_category(self):
systolic_pressure, diastolic_pressure = map(int, self.blood_pressure.split("/"))
if systolic_pressure < 120 and diastolic_pressure < 80:
return "Normal"
elif systolic_pressure < 130 and diastolic_pressure < 80:
return "Elevated"
elif systolic_pressure < 140 and diastolic_pressure < 90:
return "Stage 1 Hypertension"
else:
return "Stage 2 Hypertension"

 
The unit tests for the “Patient” class are contained in this code. Let’s examine the functions that each code component carries out:

1. Importing the Necessary Modules

  • Import unittest: This line imports the “unittest” Python module which is used to create and execute the unit tests.
  • From Patient, import Patient as pt: This line imports and renames the “Patient” class from the “Patient” module. The class that you wish to test is the “Patient” class.

2. Defining the Test Class

  • class TestPatient(unittest.TestCase): This line defines the “TestPatient” test class, an inheritor of “unittest.TestCase”. The Python test classes must inherit from “unittest.TestCase” to employ the techniques and assumptions of the testing framework.

3. Writing the Test Methods

  • test_get_blood_pressure_category_normal: The test method assesses the “get_blood_pressure_category” method’s performance in the “Normal” range, creating a “Patient” object and confirming the expected results.
  • test_get_blood_pressure_category_elevated: This test method evaluates the behavior of individuals with elevated blood pressure.
  • test_get_blood_pressure_category_hypertension_stage1 and test_get_blood_pressure_category_hypertension_stage2: These test procedures pertain to the classifications of “Stage 1 Hypertension” and “Stage 2 Hypertension”, respectively.

4. Creating the Patient Objects

  • Each test method creates a “Patient” object (patient_obj) with specific data such as the patient’s name, age, and blood pressure.

5. Calling the Get_Blood_Pressure_Category Method

  • The get_blood_pressure_category method is invoked on the patient_obj after creating the “Patient” object to determine the blood pressure category.

6. Asserting the Expected Results

  • To verify that the blood pressure category that is supplied by the “get_blood_pressure_category” method corresponds to the anticipated category, use the “self.assertEqual” method.

7. Running the Tests

  • Unittest.main() makes sure that the tests are run when the script is launched directly if __name__ == ‘__main__’.

 
PatientBDTests.py:

import unittest
from Patient import Patient as pt  # Import your Patient class
class TestPatient(unittest.TestCase):
    def test_get_blood_pressure_category_normal(self):
        patient_obj = pt("John Doe", 30, "110/70")
        actual_blood_pressure_category = patient_obj.get_blood_pressure_category()
        expected_blood_pressure_category = "Normal"
        self.assertEqual(expected_blood_pressure_category, actual_blood_pressure_category)
    def test_get_blood_pressure_category_elevated(self):
        patient_obj = pt("Jane Smith", 40, "125/75")
        actual_blood_pressure_category = patient_obj.get_blood_pressure_category()
        expected_blood_pressure_category = "Elevated"
        self.assertEqual(expected_blood_pressure_category, actual_blood_pressure_category)

    def test_get_blood_pressure_category_hypertension_stage1(self):
        patient_obj = pt("Alice Johnson", 50, "135/85")
        actual_blood_pressure_category = patient_obj.get_blood_pressure_category()
        expected_blood_pressure_category = "Stage 1 Hypertension"
        self.assertEqual(expected_blood_pressure_category, actual_blood_pressure_category)
    def test_get_blood_pressure_category_hypertension_stage2(self):
        patient_obj = pt("Bob Wilson", 60, "155/95")
        actual_blood_pressure_category = patient_obj.get_blood_pressure_category()
        expected_blood_pressure_category = "Stage 2 Hypertension"
        self.assertEqual(expected_blood_pressure_category, actual_blood_pressure_category)
if __name__ == '__main__':
    unittest.main()

 
When we run the following code, the output is produced by the code:

Conclusion

Unit testing in Python ensures the code reliability and accuracy by writing concise tests for different parts or functionalities. It helps to identify the issues, prevents regressions, maintains the code quality, and encourages the clean coding, modular design, and improved documentation.

About the author

Kalsoom Bibi

Hello, I am a freelance writer and usually write for Linux and other technology related content