Python Testing
Learn how to automatically verify Python programs using test cases, assertions, unittest, pytest, exception testing, edge-case testing and professional testing strategies.
Learning Objectives
By the end of this lesson, you will understand how automated testing helps developers verify that their Python programs continue to work correctly.
You will learn how to:
- Understand the purpose of software testing.
- Write basic automated tests.
- Use Python's
unittestframework. - Use assertions to verify results.
- Test multiple scenarios.
- Test exceptions and edge cases.
- Understand the basics of pytest.
- Understand test fixtures.
- Learn what mocking means.
- Understand test coverage.
- Develop professional testing habits.
What Is Testing?
Software testing is the process of checking whether a program behaves as expected.
Instead of manually checking every result, developers can write automated tests that verify program behavior.
Function
↓
Test Input
↓
Program Executes
↓
Actual Result
↓
Compare with Expected Result
For example, suppose we create a function that adds two numbers.
def add(a, b):
return a + b
We can test that:
add(2, 3) == 5
Key Idea
A test describes behavior that your program is expected to maintain.
Why Testing Matters
Imagine an ecommerce application with thousands of customers and orders.
A developer changes the discount calculation. The new code appears to work, but it accidentally breaks the checkout calculation.
Automated tests can detect this problem before the application reaches customers.
Testing Helps Developers
- Catch bugs early.
- Prevent regressions.
- Refactor code more safely.
- Document expected behavior.
- Improve confidence in releases.
- Find edge cases.
This is especially important in large projects where changing one component can affect many others.
Types of Testing
Software can be tested at different levels.
Unit Testing
Tests a small piece of functionality, usually a function or class.
Integration Testing
Tests whether multiple components work correctly together.
System Testing
Tests the complete application as a system.
End-to-End Testing
Tests a complete user workflow from beginning to end.
Regression Testing
Checks that previously working functionality still works after changes.
For This Lesson
We will focus primarily on automated unit testing because it provides an excellent foundation for understanding software testing.
Python's unittest
Python includes a built-in testing framework called
unittest.
Because it is part of Python's standard library, no separate installation is required.
import unittest
Tests are normally placed inside classes that inherit
from unittest.TestCase.
import unittest
class TestMath(unittest.TestCase):
def test_addition(self):
self.assertEqual(2 + 3, 5)
Your First Test
Let's create a simple application function.
def add(a, b):
return a + b
Now create a test:
import unittest
def add(a, b):
return a + b
class TestAdd(unittest.TestCase):
def test_add(self):
self.assertEqual(add(2, 3), 5)
if __name__ == "__main__":
unittest.main()
Run the file from your terminal:
python test_math.py
If the test passes, Python reports a successful test run.
Important Pattern
Test methods normally begin with
test_ so that the test framework can
discover them.
Assertions
Assertions compare actual behavior with expected behavior.
assertEqual()
self.assertEqual(add(2, 3), 5)
assertNotEqual()
self.assertNotEqual(add(2, 3), 10)
assertTrue()
self.assertTrue(10 > 5)
assertFalse()
self.assertFalse(5 > 10)
assertIsNone()
self.assertIsNone(None)
Think of Assertions as Questions
"Is the actual result what I expected?"
If the answer is no, the test fails.
Multiple Tests
A single function can have many tests.
import unittest
def multiply(a, b):
return a * b
class TestMultiply(unittest.TestCase):
def test_positive_numbers(self):
self.assertEqual(multiply(3, 4), 12)
def test_zero(self):
self.assertEqual(multiply(10, 0), 0)
def test_negative_number(self):
self.assertEqual(multiply(-2, 5), -10)
if __name__ == "__main__":
unittest.main()
Each test checks a different scenario.
Good Test Suite
A strong test suite checks normal inputs, boundary values, invalid inputs and important edge cases.
setUp() and tearDown()
Some tests need preparation before they run.
setUp() runs before each test method.
tearDown() runs after each test method.
import unittest
class TestUser(unittest.TestCase):
def setUp(self):
self.username = "Ali"
self.age = 25
def tearDown(self):
print("Test finished")
def test_username(self):
self.assertEqual(self.username, "Ali")
def test_age(self):
self.assertEqual(self.age, 25)
if __name__ == "__main__":
unittest.main()
This is useful when several tests need the same preparation.
Test Discovery
Testing frameworks need to know which files and methods contain tests.
With unittest, test methods usually begin
with:
test_
For example:
def test_login():
...
def test_checkout():
...
A method named:
def check_login():
...
will not normally be discovered as a unittest test method.
Introduction to pytest
pytest is one of the most popular Python testing frameworks.
Unlike unittest, pytest allows you to write
concise tests using normal Python functions and
assert.
Install it using pip:
python -m pip install pytest
A simple pytest test:
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
Run tests with:
pytest
unittest vs pytest
unittest is built into Python and uses
a class-based structure. pytest is a third-party
framework with a concise syntax and a rich plugin
ecosystem.
pytest Assertions
pytest uses Python's normal assert
statement.
def square(number):
return number * number
def test_square():
assert square(5) == 25
You can write multiple tests:
def test_square_positive():
assert square(4) == 16
def test_square_zero():
assert square(0) == 0
def test_square_negative():
assert square(-3) == 9
This makes pytest tests very readable.
Testing Exceptions
Good software should behave correctly when invalid data is supplied.
Consider:
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
With unittest, we can verify that the expected exception occurs.
import unittest
class TestDivide(unittest.TestCase):
def test_zero_division(self):
with self.assertRaises(ValueError):
divide(10, 0)
Why Test Exceptions?
Exceptions are part of a program's expected behavior. Testing them ensures invalid situations are handled correctly.
Testing Edge Cases
An edge case is an unusual or boundary situation that could expose a problem.
Suppose we have:
def calculate_average(numbers):
return sum(numbers) / len(numbers)
Normal input:
calculate_average([10, 20, 30])
But what happens with:
calculate_average([])
That produces a division-by-zero problem.
Other edge cases might include:
- Empty lists
- Zero values
- Negative numbers
- Very large values
- Duplicate values
- Missing data
- Unexpected strings
Testing Functions
Functions are excellent candidates for unit testing because they often have clear inputs and outputs.
def calculate_discount(price, percentage):
return price - (price * percentage / 100)
Possible tests:
def test_no_discount():
assert calculate_discount(100, 0) == 100
def test_ten_percent_discount():
assert calculate_discount(100, 10) == 90
def test_full_discount():
assert calculate_discount(100, 100) == 0
Think in Examples
Before writing tests, list the situations your function should support. Each important situation can become a test.
Testing Data
Python applications frequently process lists, dictionaries and structured records.
def get_names(users):
return [user["name"] for user in users]
Test it using representative data:
def test_get_names():
users = [
{"name": "Ali", "age": 25},
{"name": "Sara", "age": 22}
]
assert get_names(users) == ["Ali", "Sara"]
Also test an empty collection:
def test_empty_users():
assert get_names([]) == []
Introduction to Mocking
Sometimes your function depends on an external system, such as a database, API or payment service.
Testing against the real service can be slow, unreliable or expensive.
Mocking allows developers to replace a real dependency with a controlled test object.
Database
↓
Real application
During testing:
Mock Database
↓
Application
Python provides tools such as
unittest.mock.
from unittest.mock import Mock
payment_service = Mock()
payment_service.process.return_value = True
result = payment_service.process(500)
print(result)
Why Mock?
- Control external responses.
- Avoid real network requests.
- Make tests faster.
- Test failure scenarios safely.
Test Coverage
Test coverage describes how much of your code is exercised by tests.
For example, imagine a function with four logical branches:
if condition_a:
...
elif condition_b:
...
elif condition_c:
...
else:
...
If your tests only execute two branches, some behavior remains untested.
Important
High test coverage does not automatically mean high-quality tests. Tests must verify meaningful behavior, not merely execute lines of code.
Professional Testing Strategies
Arrange
Prepare the input and environment.
Act
Execute the function or operation being tested.
Assert
Verify the result.
# Arrange
price = 100
discount = 20
# Act
result = calculate_discount(price, discount)
# Assert
assert result == 80
This pattern is often called Arrange–Act–Assert.
Another Important Principle
Tests should generally be independent. One test should not rely on another test having executed first.
Common Testing Mistakes
1. Testing Only the Happy Path
Do not test only normal successful input. Include invalid and boundary cases.
2. Writing Tests That Depend on Each Other
Each test should ideally be able to run independently.
3. Testing Implementation Instead of Behavior
Tests should primarily verify what the program does, not unnecessary internal details.
4. Ignoring Failed Tests
A failing test is valuable information. Investigate it.
5. Creating Huge Tests
Smaller focused tests are easier to understand and diagnose.
Remember
A test that always passes but checks nothing useful provides very little protection.
Practice Exercises
Exercise 1 — Test a Function
def is_even(number):
return number % 2 == 0
Write tests for:
- A positive even number
- A positive odd number
- Zero
- A negative number
Exercise 2 — Test a Discount
def discount(price, percentage):
return price - (price * percentage / 100)
Create tests for 0%, 10%, 50% and 100% discounts.
Exercise 3 — Test an Exception
Create a function that raises
ValueError when an age is negative.
Then write a test that verifies the exception.
Exercise 4 — Edge Cases
Write tests for a function that accepts a list of numbers and returns the largest number.
Consider what should happen with an empty list.
Mini Challenge — Ecommerce Order Testing
Imagine you have an ecommerce order calculator:
def calculate_order_total(price, quantity, discount=0):
subtotal = price * quantity
discount_amount = subtotal * discount / 100
return subtotal - discount_amount
Your Mission
Build a test suite that verifies the function behaves correctly.
- Test a normal order.
- Test quantity of 1.
- Test quantity of 0.
- Test no discount.
- Test a 10% discount.
- Test a 100% discount.
- Test decimal prices.
Example Test
def test_normal_order():
result = calculate_order_total(100, 3, 10)
assert result == 270
Challenge Extension
Improve the function so invalid values such as a negative price or negative quantity raise an appropriate exception.
Then write tests specifically for those exceptions.
Knowledge Check
1. What is software testing?
2. Which testing framework is included with Python?
3. What does an assertion do?
4. Which prefix is normally used for unittest test methods?
5. What is an edge case?
6. What is mocking useful for?
7. What does Arrange–Act–Assert describe?
8. Does high test coverage automatically mean high-quality tests?
Lesson Summary
Automated testing allows developers to verify program behavior consistently and catch problems before they reach users.
What You Learned
- Testing verifies that software behaves as expected.
- Unit tests focus on small pieces of functionality.
-
Python includes the
unittestframework. - pytest provides a concise and powerful alternative.
- Assertions compare actual and expected results.
- Tests should include normal and edge-case scenarios.
- Exceptions can and should be tested.
- Mocking can isolate external dependencies.
- Test coverage measures how much code is exercised.
- High coverage does not automatically mean good tests.
- Arrange–Act–Assert provides a useful testing structure.
You now have the foundation for building reliable Python applications. In the next lesson, we move into databases and learn how Python can work with SQLite.
Ready to Continue?
You have completed the Python Testing lesson.