Testing thoroughly¶
Welcome to the second tutorial on unit testing! In the first tutorial, you learned to write a simple test. Now you'll learn how to test thoroughly by checking multiple scenarios and understanding edge cases.
Time commitment: 15–20 minutes
Prerequisites: Complete Your First Test before starting this tutorial.
Learning objectives¶
By the end of this tutorial, you will be able to:
- Test multiple scenarios for the same function
- Identify and test edge cases
- Use different assertion methods (
assertAlmostEqual,assertIsNone) - Understand what makes a comprehensive test suite
- Apply testing to realistic, real-world functions
Testing multiple scenarios¶
Good tests check different scenarios. Let's start with our add() function from the first tutorial and test it more thoroughly:
def add(a, b):
"""Add two numbers and return the result."""
return a + b
We already tested positive numbers. But what about:
- Negative numbers
- Zero
- Floating-point numbers
Let's create a comprehensive test suite:
import unittest
class TestAddFunctionComplete(unittest.TestCase):
"""Comprehensive tests for the add() function."""
def test_add_positive_numbers(self):
"""Test adding two positive numbers."""
self.assertEqual(add(2, 3), 5)
def test_add_negative_numbers(self):
"""Test adding two negative numbers."""
self.assertEqual(add(-2, -3), -5)
def test_add_with_zero(self):
"""Test adding zero to a number."""
self.assertEqual(add(5, 0), 5)
self.assertEqual(add(0, 5), 5)
def test_add_floats(self):
"""Test adding floating-point numbers."""
result = add(2.5, 3.7)
self.assertAlmostEqual(result, 6.2, places=1)
New assertion: assertAlmostEqual¶
Notice we used a different assertion for floats:
assertAlmostEqual(): Checks if two floats are approximately equal (useful because floating-point arithmetic can have tiny rounding errors)
The places parameter specifies how many decimal places to check.
# Run all the tests
unittest.main(argv=[''], verbosity=2, exit=False)
Understanding edge cases¶
Edge cases are scenarios at the boundaries of what your function handles. Testing edge cases helps catch bugs that only appear in unusual situations.
For testing, think about:
- The happy path: Everything works as expected
- Edge cases: Boundary values, empty inputs, zero values
- Error conditions: Invalid inputs, unexpected situations
Let's look at a more realistic example.
A realistic example: Discount calculation¶
Let's move beyond simple arithmetic and look at a function you might actually write in real code.
Imagine you're building an e-commerce system and need to calculate discounts:
def calculate_discount(price, discount_percent):
"""Calculate the final price after applying a discount.
Args:
price: Original price in pounds
discount_percent: Discount as a percentage (e.g., 20 for 20%)
Returns:
Final price after discount, or None if inputs are invalid
"""
if price < 0 or discount_percent < 0:
return None
if discount_percent > 100:
return None
discount_amount = price * (discount_percent / 100)
return price - discount_amount
Comprehensive tests for discount calculation¶
Let's write tests that cover:
- Normal cases: Standard discount scenarios
- Edge cases: Zero discount, full discount
- Error conditions: Invalid inputs
class TestDiscountCalculation(unittest.TestCase):
"""Tests for discount calculation - a common real-world requirement."""
def test_normal_discount(self):
"""Test that a 20% discount on £100 results in £80."""
self.assertAlmostEqual(calculate_discount(100, 20), 80.0, places=2)
def test_no_discount(self):
"""Test that zero discount returns original price."""
self.assertEqual(calculate_discount(100, 0), 100.0)
def test_full_discount(self):
"""Test that 100% discount results in zero."""
self.assertEqual(calculate_discount(50, 100), 0.0)
def test_invalid_negative_price(self):
"""Test that negative price returns None."""
self.assertIsNone(calculate_discount(-10, 20))
def test_invalid_negative_discount(self):
"""Test that negative discount returns None."""
self.assertIsNone(calculate_discount(100, -5))
def test_invalid_excessive_discount(self):
"""Test that discount over 100% returns None."""
self.assertIsNone(calculate_discount(100, 150))
# Run the discount calculation tests
unittest.main(argv=[''], verbosity=2, exit=False)
New assertion: assertIsNone¶
We introduced another useful assertion:
assertIsNone(): Checks if a value isNone
This is perfect for testing error conditions where your function returns None to indicate invalid input.
What makes tests comprehensive?¶
Notice how our discount tests check:
- Normal behaviour (
test_normal_discount) - Boundary values (
test_no_discount,test_full_discount) - Invalid inputs (negative values, excessive discounts)
This three-pronged approach is a key principle of thorough testing:
- Test the happy path (everything works as expected)
- Test edge cases (boundary values, empty inputs)
- Test error conditions (invalid inputs, unexpected situations)
How many tests are enough?¶
There's no magic number, but ask yourself:
- Have I tested typical usage?
- Have I tested boundary conditions?
- Have I tested error cases?
- Would I be confident deploying this code?
If you answer "yes" to all of these, you probably have enough tests.
Exercise: Test a temperature converter¶
Now it's your turn! Here's a function that converts Celsius to Fahrenheit:
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
Your task: Write comprehensive tests covering:
- Normal temperature (e.g., 20°C should be 68°F)
- Freezing point (0°C should be 32°F)
- Boiling point (100°C should be 212°F)
- Negative temperatures (e.g., -40°C should be -40°F)
Try it yourself before looking at the solution!
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
# Write your test class here
class TestTemperatureConverter(unittest.TestCase):
"""Your tests for the celsius_to_fahrenheit() function."""
def test_normal_temperature(self):
"""Test converting a typical temperature."""
# TODO: Test that 20°C equals 68°F
pass
def test_freezing_point(self):
"""Test the freezing point of water."""
# TODO: Test that 0°C equals 32°F
pass
def test_boiling_point(self):
"""Test the boiling point of water."""
# TODO: Test that 100°C equals 212°F
pass
def test_negative_temperature(self):
"""Test negative temperatures."""
# TODO: Test that -40°C equals -40°F (interesting coincidence!)
pass
Solution¶
Here's one possible solution (try yours first!):
class TestTemperatureConverterSolution(unittest.TestCase):
"""Solution: Tests for the celsius_to_fahrenheit() function."""
def test_normal_temperature(self):
"""Test converting a typical temperature."""
self.assertAlmostEqual(celsius_to_fahrenheit(20), 68.0, places=1)
def test_freezing_point(self):
"""Test the freezing point of water."""
self.assertAlmostEqual(celsius_to_fahrenheit(0), 32.0, places=1)
def test_boiling_point(self):
"""Test the boiling point of water."""
self.assertAlmostEqual(celsius_to_fahrenheit(100), 212.0, places=1)
def test_negative_temperature(self):
"""Test negative temperatures."""
self.assertAlmostEqual(celsius_to_fahrenheit(-40), -40.0, places=1)
def test_absolute_zero(self):
"""Bonus: Test absolute zero."""
self.assertAlmostEqual(celsius_to_fahrenheit(-273.15), -459.67, places=1)
# Run the solution tests
unittest.main(argv=[''], verbosity=2, exit=False)
Common assertions reference¶
Here's a quick reference of assertions we've used so far:
| Assertion | Checks | Example |
|---|---|---|
assertEqual(a, b) |
a == b |
self.assertEqual(add(2, 3), 5) |
assertAlmostEqual(a, b) |
round(a-b, 7) == 0 |
self.assertAlmostEqual(0.1 + 0.2, 0.3) |
assertIsNone(x) |
x is None |
self.assertIsNone(error_function()) |
We'll learn more assertions in the next tutorial!
Key takeaways¶
Congratulations! You've learned to test thoroughly. Let's recap:
- Test multiple scenarios - not just the happy path
- Edge cases matter - test boundary conditions and unusual inputs
- Use appropriate assertions -
assertAlmostEqualfor floats,assertIsNonefor None checks - Comprehensive tests cover normal cases, edge cases, and error conditions
- Real-world functions need thorough testing to catch bugs before production
Next steps¶
Now that you understand how to test thoroughly, learn best practices and common pitfalls:
- Testing Best Practices - Learn common assertions, avoid mistakes, and establish good testing habits
Supporting resources¶
- unittest Quick Reference - Complete assertion guide
Remember: Thorough testing gives you confidence that your code works correctly in all situations.
Happy testing!