07 Exceptions - 03 Unit Testing
Unit testing is a software testing technique where individual units or components of a program are tested in isolation. The goal is to ensure that each function or module works as expected. Unit testing helps catch errors early, provides better documentation, and ensures that the code continues to work correctly as it evolves.
Python provides several ways to perform unit testing. Here, we will focus on the assert
keyword, manual error handling, and the powerful testing framework pytest
.
1. Basic Unit Test Using assert
The assert
keyword is used to verify that an expression evaluates to True
. If the expression is False
, an AssertionError
is raised. This is a simple way to test the correctness of your code.
Example 1: Testing a Simple Function with assert
Let’s say we have a function square(x)
that returns the square of a number.
Function to be tested:
# calculator.py
def square(x):
return x ** 2
Unit Test Code:
from calculator import square # Importing the function to test
def main():
test_square()
def test_square():
# Using assert to check the correctness of the function
assert square(2) == 4 # 2 squared is 4
assert square(3) == 9 # 3 squared is 9
if __name__ == "__main__":
main()
Explanation:
assert square(2) == 4
: This checks if the functionsquare(2)
returns 4. If it doesn’t, anAssertionError
will be raised.- Using
assert
is preferable to manually checking the condition and printing messages because it provides immediate feedback during testing and is easier to automate.
Outcome:
- If both assertions are true, the program will exit quietly, meaning no errors were encountered.
- If any assertion fails, an
AssertionError
will occur, and the test will be marked as failed.
2. Handling Errors Using try-except
While assert
is simple and effective for basic testing, you might want to handle errors more explicitly. You can do this using try-except
blocks.
Example 2: Handling Assertion Errors with try-except
from calculator import square
def main():
test_square()
def test_square():
try:
assert square(2) == 4
except AssertionError:
print("2 squared was not 4")
try:
assert square(3) == 9
except AssertionError:
print("3 squared was not 9")
if __name__ == "__main__":
main()
Explanation:
- Here, we manually handle
AssertionError
exceptions usingtry-except
blocks. - If the assertion fails, a custom error message will be printed.
Outcome:
- This method gives you more control over the output, but it’s more verbose and less automated than using
assert
alone.
3. Using pytest
for Automated Testing
pytest
is a popular testing framework in Python that can be used to write more sophisticated tests. It handles error reporting, test isolation, and much more.
Installing pytest
:
To use pytest
, install it using pip:
pip install pytest
Example 3: Testing with pytest
# test_calculator.py
from calculator import square
def test_square():
assert square(2) == 4 # Test for positive number
assert square(-2) == 4 # Test for negative number
assert square(3) == 9 # Another test for positive number
assert square(0) == 0 # Test for zero
Running the Test:
To run the test, execute the following command in the terminal:
pytest test_calculator.py
Explanation:
pytest
automatically identifies functions prefixed withtest_
as test cases.- It handles the assertion and error reporting. If any assertion fails,
pytest
will print detailed error messages to help you debug.
Outcome:
pytest
gives a clear, easy-to-read output and can handle large suites of tests efficiently.- It automatically discovers and runs the test functions.
4. Reducing Side Effects for Better Testing
One of the challenges with testing code is dealing with functions that have “side effects”—such as printing to the console or modifying global variables. For unit tests to be effective, it’s best to minimize these side effects.
Example 4: Avoiding Side Effects
Before:
def hello(to="world"):
print("Hello,", to)
def main():
name = input("What's your name? ")
hello(name)
This function hello()
prints a message to the console. When testing it, the output isn’t easy to validate because it doesn’t return anything, making it difficult to check the output programmatically.
After:
To make this function more testable, we can modify it to return the string instead of printing it.
def hello(to="world"):
return f"Hello, {to}"
def main():
name = input("What's your name? ")
print(hello(name)) # Print the returned value
Now, the hello()
function returns the string, which makes it easy to test.
Example 5: Testing the New Version of hello()
# test_hello.py
from hello import hello
def test_hello():
assert hello("David") == "Hello, David" # Testing with name provided
assert hello() == "Hello, world" # Testing with default value
Explanation:
- The modified version of
hello()
returns the string rather than printing it. - Now, the test checks the returned values, which is easier to validate and automate.
Best Practices for Unit Testing
1. Write Small Functions
Break your code into small, focused functions. Smaller functions are easier to test, debug, and maintain. Each function should ideally have a single responsibility and clear input/output.
2. Avoid Side Effects
Side effects, like modifying global variables or printing output, should be minimized. Functions should return values rather than print them, making them easier to test and reuse.
3. Use Descriptive Test Names
Name your test functions clearly. The name should describe the behavior being tested (e.g., test_square_positive_numbers()
).
4. Test for Edge Cases
Make sure to test edge cases and unexpected inputs, such as negative numbers, zero, or very large numbers.
5. Automate Testing
Use tools like pytest
to automate your tests. This saves time, especially when you have many tests and want to run them regularly.
6. Separate Tests from Code
Keep your tests in separate files or modules, such as test_calculator.py
, so they can be managed independently from your main program.