Lesson 59 · Python Development Tools 45–55 minutes Intermediate

Python Testing

Learn how to automatically verify Python programs using test cases, assertions, unittest, pytest, exception testing, edge-case testing and professional testing strategies.

0%
01

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 unittest framework.
  • 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.
02

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.

03

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.

04

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.

05

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)
06

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.

07

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.

08

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.

09

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.

10

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.

11

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.

12

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.

13

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.

14

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
15

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.

16

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([]) == []
17

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.
18

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.

19

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.

20

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.

21

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.

22

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.

  1. Test a normal order.
  2. Test quantity of 1.
  3. Test quantity of 0.
  4. Test no discount.
  5. Test a 10% discount.
  6. Test a 100% discount.
  7. 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.

23

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?

24

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 unittest framework.
  • 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.