DEV Community

Cover image for Boost Your Python Unit Tests: A Beginner's Guide to Abstraction ๐Ÿš€
Retiago Drago
Retiago Drago

Posted on โ€ข Edited on

Boost Your Python Unit Tests: A Beginner's Guide to Abstraction ๐Ÿš€

Outlines

Introduction ๐ŸŒŸ

Hello! I am currently on my journey to understand the powerful tool that is unit testing in Python. I recently came across an enlightening tutorial from PythonTutorial.net, which provided a great starting point. However, as someone new to this field, I noticed a few areas where things could potentially be made more efficient. So, let's dive into it!

Note: I've recently gained new knowledge about unit testing, and I'm excited to share it with you. Please consider that the information below represents my current understanding, which may evolve over time. I'll continue to update and refine this post as I learn more about the intricacies of unit testing. Your understanding and patience are greatly appreciated.

Python Test Fixtures Example ๐Ÿค–

Consider a BankAccount class that we used for our first exercise:

Bank Account class:

class InsufficientFund(Exception):
pass
class BankAccount:
def __init__(self, balance: float) -> None:
if balance < 0:
raise ValueError('balance cannot be negative')
self._balance = balance
@property
def balance(self) -> float:
return self._balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError('The amount must be positive')
self._balance += amount
def withdraw(self, amount: float) -> None:
if amount <= 0:
raise ValueError('The withdrawal amount must be more than 0')
if amount > self._balance:
raise InsufficientFund('Insufficient ammount for withdrawal')
self._balance -= amount

We set up our original test class for the BankAccount as follows:

import unittest
from bank_account import BankAccount, InsufficientFund
class TestBankAccount(unittest.TestCase):
def setUp(self) -> None:
self.bank_account = BankAccount(100)
def test_deposit(self):
self.bank_account.deposit(100)
self.assertEqual(self.bank_account.balance, 200)
def test_withdraw(self):
self.bank_account.withdraw(50)
self.assertEqual(self.bank_account.balance, 50)
def tearDown(self) -> None:
self.bank_account = None

This certainly works, but can we do better? Yes, I believe we can. Here's how we can improve this:

  1. Avoid unnecessary object deletions: In our tearDown methods, we are currently setting the objects to None. However, Python's garbage collection would automatically handle the deletion of objects that are no longer in use. Thus, we could focus more on ensuring that your tests are independent and do not share mutable state to avoid unexpected side effects.

  2. Replace direct answers with calculated results: Instead of using hard-coded values for assertions, using calculations or variables can make our tests more robust and adaptable to changes. This approach allows for flexibility when modifying your code in the future.

My suggestion:

import unittest
from bank_account import BankAccount, InsufficientFund
# Use constants for initial balance and deposit/withdrawal amounts
INIT_BALANCE = 100
DEPOSIT_AMOUNT = 100
WITHDRAW_AMOUNT = 50
class TestBankAccount(unittest.TestCase):
def setUp(self) -> None:
self.bank_account = BankAccount(INIT_BALANCE)
def test_deposit(self):
self.bank_account.deposit(DEPOSIT_AMOUNT)
# Use calculations in assertions instead of hard-coded values
self.assertEqual(self.bank_account.balance, INIT_BALANCE + DEPOSIT_AMOUNT)
def test_withdraw(self):
self.bank_account.withdraw(WITHDRAW_AMOUNT)
# Use calculations in assertions instead of hard-coded values
self.assertEqual(self.bank_account.balance, INIT_BALANCE - WITHDRAW_AMOUNT)
def tearDown(self) -> None:
# Instead of setting to None, let Python's garbage collection handle it
del self.bank_account

Indeed, one can enhance their unit tests further by incorporating helper functions, especially when dealing with complex arithmetic operations. Helper functions improve the readability of your code and make it easier to maintain. However, be mindful that overuse of helper functions for simple operations like addition and subtraction could potentially convolute your code more than clarify it. The goal is to strike a balance between readability and simplicity, ensuring that your test suite remains straightforward and easy to understand, maintain, and debug.

Transitioning from our BankAccount example, let's dive into a more complex scenario to illustrate this concept effectively.

Abstracting Arithmetic Operations for Better Testing ๐Ÿคš

Let's look at Heron's formula, a method for calculating the area of a triangle, which involves a blend of arithmetic operations such as multiplication and subtraction. This is a scenario where abstracting the arithmetic operations into helper functions can shine and significantly improve the readability and maintainability of your code. Here's how it could look:

import math
class Triangle:
def __init__(self, a: float, b: float, c: float):
self.a = a
self.b = b
self.c = c
def perimeter(self):
return self.a + self.b + self.c
def semi_perimeter(self):
return self.perimeter() / 2
def area(self):
s = self.semi_perimeter()
return math.sqrt(s * (s - self.a) * (s - self.b) * (s - self.c))

And here's the corresponding test class with helper functions:

import math
import unittest
from triangle import Triangle
# Use constants for initial sides
A = 5
B = 12
C = 13
def sum(*args):
return math.fsum(args)
def subtract(a, b):
return a - b
def multiply(*args):
result = 1
for num in args:
result *= num
return result
def sqrt(a):
return math.sqrt(a)
class TestTriangle(unittest.TestCase):
def setUp(self) -> None:
self.triangle = Triangle(A, B, C)
def test_perimeter(self):
self.assertEqual(self.triangle.perimeter(), sum(A, B, C))
def test_semi_perimeter(self):
self.assertEqual(self.triangle.semi_perimeter(), sum(A, B, C) / 2)
def test_area(self):
s = self.triangle.semi_perimeter()
self.assertEqual(self.triangle.area(), sqrt(multiply(s, subtract(s, A), subtract(s, B), subtract(s, C))))
def tearDown(self) -> None:
del self.triangle

In this case, using helper functions to abstract the arithmetic operations makes the assertions easier to read. Also, since Heron's formula involves both multiplication and subtraction, having separate functions for these operations increases the readability of the test_area method.

And yes, one of the benefits of this approach is composability. For example, the semi_perimeter function is used as part of the area function, and this is reflected in the tests as well. This enhances the maintainability of the test suite, as changes to the computation of the semi-perimeter would only require updates to the semi_perimeter and area tests, not the perimeter test.

Conclusion ๐Ÿคโœ…

Unit testing is a critical instrument in any software development process, and becoming proficient at it is indeed an ongoing journey. The insights I've shared today are inspired by my own beginner's perspective, a fresh viewpoint that often brings unique value to seasoned developers. It is my hope that sharing this journey from a junior's perspective can shed light on new insights for more seasoned coders and also prove valuable to those just starting their journey in writing Python unit tests.

Remember, it's advisable to steer clear of unnecessary object deletions in your tearDown methods - Python's garbage collection handles this efficiently. Also, consider replacing hard-coded values with calculated results to enhance the flexibility and resilience of your tests, making them better suited to adapt to future changes. This practice of ensuring your tests are robust, adaptable, and maintainable is integral for efficient unit testing and overall quality software development.

As a beginner in unit testing, I hope my insights can help others who are also starting their journey in writing Python unit tests. I am always looking to improve my skills and welcome feedback and suggestions in the comments section below. Let's learn and grow together!

Follow me on Beacons for more Python and software development insights.

ranggakd - Link in Bio & Creator Tools | Beacons

@ranggakd | center details summary summary Oh hello there I m a an Programmer AI Tech Writer Data Practitioner Statistics Math Addict Open Source Contributor Quantum Computing Enthusiast details center.

favicon beacons.ai

Top comments (0)