Beginner’s Guide to Unit Testing Python Code with PyTest


Beginner's Guide to Unit Testing Python Code with PyTest
Image by Author | Created on Canva

 

In software development, unit testing is important to check that individual components of your code work correctly. At its core, unit testing is the practice of testing individual units—functions, methods, classes—of your code to ensure they work as expected. Think of it as zooming in on small sections of your code and asking, “Does this piece of code do what it’s supposed to?”

Good unit tests help you ensure that bugs are caught early, code remains maintainable, and you can refactor without introducing new issues. In this tutorial, we’ll do the following:

  • Build a simple TO-DO list manager
  • Write unit tests for different functionalities using PyTest
  • Run tests and interpret the results

Let’s get started.

 

Setting Up the Project

 
▶️ You can find the code for this tutorial on GitHub.

Before we jump into coding, let’s set up a clean folder structure for our project. This will help organize the code and tests properly. Here’s the recommended folder structure:

todo_project/
│
├── todo/
│   ├── __init__.py
│   └── todo_manager.py
│
├── tests/
│   ├── __init__.py
│   └── test_todo_manager.py
│
└── requirements.txt

 

Here’s a quick breakdown:

  • todo/: This folder contains the core application logic: the ToDoManager class that we will write. We also add an empty __init__.py file so we can import and use functions and classes in this module.
  • tests/: This folder holds all the test files. We’ll place our unit tests here, keeping them separate from the main code.

If you have a list of dependencies, you can create a requirements.txt file and install the libraries in a virtual environment like so:

For this tutorial, we only need PyTest. So you can create and activate a virtual environment:

$ python3 -m venv v1
$ source v1/bin/activate

 

Then install PyTest using pip:

 

Now that we’ve installed PyTest, let’s start coding!

 

Creating the To-Do List Manager

 
We’ll now build the To-Do list app. The ToDoManager class allows users to add tasks, remove tasks, mark them as completed, and retrieve tasks based on their completion status:

# todo/todo_manager.py
class ToDoManager:
    def __init__(self):
        self.tasks = []

    def add_task(self, task):
        if not task:
            raise ValueError("Task cannot be empty.")
        self.tasks.append("task": task, "completed": False)
        return task

    def remove_task(self, task):
        for t in self.tasks:
            if t["task"] == task:
                self.tasks.remove(t)
                return task
        raise ValueError("Task not found.")

    def mark_completed(self, task):
        for t in self.tasks:
            if t["task"] == task:
                t["completed"] = True
                return task
        raise ValueError("Task not found.")
    
    def get_tasks(self, completed=None):
        if completed is None:
            return self.tasks
        return [t for t in self.tasks if t["completed"] == completed]

 

The add_task method adds a new task to the list. It raises an error if the task is empty. The remove_task method removes a task from the list. Raises an error if the task doesn’t exist.

The mark_completed marks a task as completed. It raises an error if the task is not found. The get_tasks(completed=None) method retrieves tasks from the list. You can filter by completed or pending tasks using the completed parameter.

 

Writing Unit Tests with PyTest

 
Now that we have a working class, it’s time to write unit tests for it. We will write our tests in the tests/test_todo_manager.py file. Let’s write tests for each method in our ToDoManager class.

We create a fixture that initializes the ToDoManager before each test. This ensures that each test has a clean instance of the class and prevents side effects.

# tests/test_todo_manager.py
import pytest
from todo.todo_manager import ToDoManager

@pytest.fixture
def todo_manager():
    return ToDoManager()

 

It’s now time to write the test cases—each test case focusing on one method:

Method Description
test_add_task Tests if tasks are added correctly
test_add_empty_task Ensures that an empty task raises an error
test_remove_task Checks if a task can be successfully removed
test_remove_nonexistent_task Tests if an error is raised when trying to remove a non-existent task
test_mark_completed Tests if tasks can be marked as completed
test_get_tasks Retrieves all tasks and filters completed and pending tasks

 

# tests/test_todo_manager.py
import pytest
from todo.todo_manager import ToDoManager

...

def test_add_task(todo_manager):
    todo_manager.add_task("Buy groceries")
    assert todo_manager.tasks == ["task": "Buy groceries", "completed": False]

def test_add_empty_task(todo_manager):
    with pytest.raises(ValueError, match="Task cannot be empty."):
        todo_manager.add_task("")

def test_remove_task(todo_manager):
    todo_manager.add_task("Buy groceries")
    todo_manager.remove_task("Buy groceries")
    assert todo_manager.tasks == []

def test_remove_nonexistent_task(todo_manager):
    with pytest.raises(ValueError, match="Task not found."):
        todo_manager.remove_task("Do laundry")

def test_mark_completed(todo_manager):
    todo_manager.add_task("Go for a walk")
    todo_manager.mark_completed("Go for a walk")
    assert todo_manager.tasks == ["task": "Go for a walk", "completed": True]

def test_get_tasks(todo_manager):
    todo_manager.add_task("Task 1")
    todo_manager.add_task("Task 2")
    todo_manager.mark_completed("Task 1")
    
    all_tasks = todo_manager.get_tasks()
    completed_tasks = todo_manager.get_tasks(completed=True)
    pending_tasks = todo_manager.get_tasks(completed=False)
    
    assert len(all_tasks) == 2
    assert completed_tasks == ["task": "Task 1", "completed": True]
    assert pending_tasks == ["task": "Task 2", "completed": False]

 

Running the Tests

 
Let’s now run the tests. Navigate to the root of your project (here todo_project) and run the following command:

 

PyTest will auto-discover the tests in the tests/ folder and execute them. If everything is correct, you should see output like this:

============================= test session starts ==============================
platform linux -- Python 3.11.4, pytest-8.3.3, pluggy-1.5.0
rootdir: /home/balapriya/pytest_t
collected 6 items                                                              

tests/test_todo_manager.py ......                                        [100%]

============================== 6 passed in 0.02s ===============================

 

This means all our tests passed successfully!

 

Wrapping Up

 
Unit tests allow you to catch bugs early in the development cycle—saving time and resources. When you make changes or refactor code, unit tests ensure that the code works as expected. Writing unit tests forces you to design your code in a modular and testable way.

In this tutorial, we walked through building a To-Do list app and writing unit tests using PyTest. By following best practices for organizing your project, you can scale your application and tests as it grows. Unit testing, especially with PyTest, makes your code more robust, maintainable, and bug-free.

Happy unit testing!
 
 

Bala Priya C is a developer and technical writer from India. She likes working at the intersection of math, programming, data science, and content creation. Her areas of interest and expertise include DevOps, data science, and natural language processing. She enjoys reading, writing, coding, and coffee! Currently, she’s working on learning and sharing her knowledge with the developer community by authoring tutorials, how-to guides, opinion pieces, and more. Bala also creates engaging resource overviews and coding tutorials.



Recent Articles

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here