Testing Examples

Author

Davide Vitiello, Mirai Solutions GmbH

Published

March 11, 2025

This section provides concrete, hands-on examples of writing modular Python code and associated test run using pytest. We will develop simple code in a few modules, then show how to structure and run the corresponding tests.

In particular, you will learn how to:

Test Examples

Unit tests

As its name suggests, a unit test focuses on one main expectation, which can be a single function, class, or module. Moreover, a unit test should normally:

  • Cover corner cases
  • Be lightweight
  • Test one piece of code at a time
  • Be repeatable
  • Be self-validating: In other words, it should result in a binary pass-or-fail outcome, not needing further human investigation to interpret the result.

As a best practice, strive to adopt a Test-First Approach, namely writing the test before the relevant piece of code. This approach allows for a more modular design, as the code is developed to pass the tests.
Following are examples of unit test implementations for different scenarios and their test runs.

Maths

Below is the implementation of a simple maths module, defining functions to add and subtract two numbers:

# src/python_training_material/calc.py
def add(a, b):
    return a + b


def subtract(a, b):
    return a - b


# other operations...

The following is the implementation of the unit tests for the maths module, wherein we test the add and subtract functions:

# tests/test_calc.py
import pytest
from python_training_material.calc import add, subtract


# Test cases for addition
@pytest.mark.parametrize("a, b, expected", [(10, 5, 15), (-2, 5, 3), (3.5, 2.5, 6)])
def test_add(a, b, expected):
    assert add(a, b) == expected


# Test cases for subtraction
@pytest.mark.parametrize("a, b, expected", [(10, 5, 5), (-2, -2, 0), (3.5, 2.5, 1)])
def test_subtract(a, b, expected):
    assert subtract(a, b) == expected
============================= test session starts ==============================
platform linux -- Python 3.12.8, pytest-8.3.4, pluggy-1.5.0
rootdir: /home/runner/work/py-techguides/py-techguides/codebase
configfile: pyproject.toml
plugins: anyio-4.8.0
collecting ... collected 6 items                                                              

tests/test_calc.py::test_add[10-5-15] PASSED                             [ 16%]
tests/test_calc.py::test_add[-2-5-3] PASSED                              [ 33%]
tests/test_calc.py::test_add[3.5-2.5-6] PASSED                           [ 50%]
tests/test_calc.py::test_subtract[10-5-5] PASSED                         [ 66%]
tests/test_calc.py::test_subtract[-2--2-0] PASSED                        [ 83%]
tests/test_calc.py::test_subtract[3.5-2.5-1] PASSED                      [100%]

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

@pytest.mark.parametrize is ideal for providing distinct input data to individual tests, whereas @pytest.fixture is better suited for reusable data across multiple tests.

I/O

Below is the implementation of an I/O module, which contains two functions that write and read data from a file:

# src/python_training_material/io.py
def write_to_file(file_path, data):
    with open(file_path, "w") as file:
        file.write(data)


def read_from_file(file_path):
    with open(file_path, "r") as file:
        return file.read()


# other I/O operations...

The following is the implementation of the unit tests for the I/O module, wherein we test both write_to_file() and read_from_file() within the same test function, to make sure that a file is written and read correctly when using our I/O functions in conjunction:

# tests/test_io.py
from python_training_material.io import read_from_file, write_to_file


def test_file_write_read(tmpdir):
    # Write to a temporary file
    test_file = tmpdir.join("test.txt")
    write_to_file(test_file, "Hello, World!")

    # Read from the same file
    content = read_from_file(test_file)
    assert content == "Hello, World!"


# Additional tests for edge cases and larger files...
============================= test session starts ==============================
platform linux -- Python 3.12.8, pytest-8.3.4, pluggy-1.5.0
rootdir: /home/runner/work/py-techguides/py-techguides/codebase
configfile: pyproject.toml
plugins: anyio-4.8.0
collecting ... collected 1 item                                                               

tests/test_io.py::test_file_write_read PASSED                            [100%]

============================== 1 passed in 0.01s ===============================

Book Library

In software development, especially in complex applications, it’s crucial to structure code in a way that promotes modularity and testability. One effective strategy is to define abstract interfaces for different components or services in your application. Since abstract interfaces define a set of method signatures without implementation, by using them in this context we decouple the implementation details from the interface, making it easier to test components in isolation and together.

Defining abstract interfaces is particularly beneficial for unit testing, where we want to test components in isolation. In fact, by relying on abstract interfaces, we can implement multiple versions of a service (e.g., mocks or stubs) to facilitate testing various scenarios without relying on concrete implementations that might have dependencies or side effects.

Let’s consider a library management system that involves multiple services:

  • Book Service: Manages the collection of books in the library.
  • User Service: Handles user registration and tracking of borrowed books.

In this scenario, we want to test the interaction between these services, such as a user borrowing a book. By defining abstract interfaces for our services, we establish a contract that all implementations must follow. This approach allows us to:

  • Easily swap implementations for testing purposes.
  • Mock or stub services during tests to isolate components.
  • Ensure consistency in how services interact with each other.

In accordance with the aforementioned, we start by defining the abstract interface for users and books, which will allow variable implementations with concrete classes via inheritance. In the following sections, we’ll use those classes to model the services of a book library management system.

Book Service Implementation

For our scenario, we define an abstract library book service class and a corresponding implementation:

# src/python_training_material/library/book_service.py
from abc import ABC, abstractmethod


class AbstractBookService(ABC):

    @abstractmethod
    def add_book(self, title: str, author: str) -> int | None:
        pass

    @abstractmethod
    def borrow_book(self, book_id: int) -> bool:
        pass

    @abstractmethod
    def return_book(self, book_id: int) -> bool:
        pass

    @abstractmethod
    def remove_book(self, book_id: int) -> bool:
        pass


class BookService(AbstractBookService):

    def __init__(self):
        self._books = {}
        self._id_counter = 1

    def add_book(self, title: str, author: str) -> int | None:
        book_id = self._id_counter
        if title == "" or author == "":
            return None
        book = {"title": title, "author": author, "available": True}
        self._books[book_id] = book
        self._id_counter += 1
        return book_id

    def get_book(self, book_id: int) -> dict | None:
        return self._books.get(book_id)

    def book_is_available(self, book_id: int) -> bool:
        book = self.get_book(book_id)
        if book:
            return book["available"]
        return False

    def search_book_by_title(self, title: str) -> dict:
        if title == "":
            return {}
        return {
            book_id: book
            for book_id, book in self._books.items()
            if title.lower() in book["title"].lower()
        }

    def borrow_book(self, book_id: int) -> bool:
        if self.book_is_available(book_id):
            self._books[book_id]["available"] = False
            return True  # Indicate successful borrow
        return False

    def return_book(self, book_id: int) -> bool:
        book = self.get_book(book_id)
        if book and not book["available"]:
            self._books[book_id]["available"] = True
            return True  # Indicate successful return
        return False

    def remove_book(self, book_id: int) -> bool:
        if self.book_is_available(book_id):
            del self._books[book_id]
            return True  # Indicate successful deletion
        return False

We then define a series of unit tests encompassing all its methods:

# tests/library/test_book_service.py
import pytest
from python_training_material.library.book_service import BookService


@pytest.fixture
def book_service():
    return BookService()


@pytest.fixture
def book_data():
    """
    Fixture to provide book data for testing.
    """
    return {"title": "1984", "author": "George Orwell"}


@pytest.fixture
def created_book_id(book_service, book_data):
    """
    Fixture to create a book and return its ID for testing.
    """
    book_id = book_service.add_book(**book_data)
    return book_id


@pytest.fixture
def borrowed_book_id(book_service, created_book_id):
    """
    Fixture that borrows the created_book_id so that
    we start the test with an already-borrowed book.
    """
    book_service.borrow_book(created_book_id)
    return created_book_id


def test_add_book_with_existing_data(book_service, book_data):
    """
    Test that a book can be added successfully.
    """
    added_book_id = book_service.add_book(**book_data)
    assert book_service.get_book(added_book_id) is not None


@pytest.mark.parametrize(
    "missing_fields",
    [
        {"title": ""},
        {"author": ""},
        {"title": "", "author": ""},
    ],
)
def test_add_book_with_missing_data(book_service, book_data, missing_fields):
    """
    Test that adding a book with missing data returns None.
    """
    book_data_modified = book_data | missing_fields
    assert book_service.add_book(**book_data_modified) is None


def test_get_book_with_existing_id(book_service, book_data, created_book_id):
    """
    Test that an existing book can be retrieved successfully.
    """
    assert book_service.get_book(created_book_id) == book_data | {"available": True}


def test_get_book_with_missing_id(book_service, missing_id=999):
    """
    Test that retrieving a book with a non-existent id fails.
    """
    # Assuming the value of `missing_id` does not represent an existing book
    assert book_service.get_book(missing_id) is None


def test_book_is_available_with_existing_id(book_service, created_book_id):
    """
    Test that a book with an existing id is available.
    """
    # Upon creation via `book_service.add_book()`, a book's `available` field is set to True
    assert book_service.book_is_available(created_book_id) is True


def test_book_is_available_with_borrowed_book(book_service, borrowed_book_id):
    """
    Test that a borrowed book with an existing id is not available.
    """
    assert book_service.book_is_available(borrowed_book_id) is False


def test_book_is_available_with_missing_id(book_service, missing_id=999):
    """
    Test that a book with a non-existent id is not available.
    """
    # Assuming the value of `missing_id` does not represent an existing book
    assert book_service.book_is_available(missing_id) is False


def test_borrow_book_with_existing_id(book_service, created_book_id):
    """
    Test that a book with an existing id can be borrowed successfully.
    """
    assert book_service.borrow_book(created_book_id) is True
    assert book_service.book_is_available(created_book_id) is False


def test_borrow_book_with_missing_id(book_service, missing_id=999):
    """
    Test that borrowing a book with a non-existent id fails.
    """
    # Assuming the value of `missing_id` does not represent an existing book
    assert book_service.borrow_book(missing_id) is False


def test_borrow_book_unavailable(book_service, borrowed_book_id):
    """
    Test that borrowing an already borrowed book returns False.
    """
    assert book_service.borrow_book(borrowed_book_id) is False


def test_search_book_by_full_title(book_service, book_data, created_book_id):
    """
    Test that searching for a book by its full title returns the correct results.
    """
    book_found = book_service.search_book_by_title(book_data["title"])
    assert book_found == {created_book_id: book_data | {"available": True}}


def test_search_book_by_partial_title(book_service, book_data, created_book_id):
    """
    Test that searching for a book by its partial title returns the correct results.
    """
    book_found = book_service.search_book_by_title(book_data["title"][:2])  # 19
    assert book_found == {created_book_id: book_data | {"available": True}}


def test_search_book_by_title_missing(book_service, missing_title="The Grinch"):
    """
    Test that searching for a book that does not exist returns an empty dictionary.
    """
    # Assuming the value of missing_title does not represent an existing book
    assert book_service.search_book_by_title(missing_title) == {}


def test_search_book_by_title_empty(book_service):
    """
    Test that searching for a book with an empty title doesn't return any results.
    """
    assert book_service.search_book_by_title("") == {}


def test_return_book_borrowed(book_service, borrowed_book_id):
    """
    Test that a book that has been borrowed can be returned successfully.
    """
    assert book_service.return_book(borrowed_book_id) is True
    assert book_service.book_is_available(borrowed_book_id) is True


def test_return_book_not_borrowed(book_service, created_book_id):
    """
    Test that returning an existing book that was not borrowed returns False.
    """
    assert book_service.return_book(created_book_id) is False


def test_return_book_with_missing_id(book_service, missing_id=999):
    """
    Test that returning a book with a non-existent id returns False.
    """
    # Assuming the value of `missing_id` does not represent an existing book
    assert book_service.return_book(missing_id) is False


def test_return_book_already_returned(book_service, borrowed_book_id):
    """
    Test that returning a book that has already been returned returns False.
    """
    book_service.return_book(borrowed_book_id)
    assert book_service.return_book(borrowed_book_id) is False


def test_remove_book_with_existing_id(book_service, created_book_id):
    """
    Test that a book with an existing id can be deleted successfully.
    """
    assert book_service.remove_book(created_book_id) is True
    assert book_service.get_book(created_book_id) is None


def test_remove_book_with_missing_id(book_service, missing_id=999):
    """
    Test that removing a book with a non-existent id returns False.
    """
    # Assuming the value of `missing_id` does not represent an existing book
    assert book_service.remove_book(missing_id) is False


def test_remove_book_with_borrowed_id(book_service, borrowed_book_id):
    """
    Test that removing a borrowed book returns False.
    """
    assert book_service.remove_book(borrowed_book_id) is False
============================= test session starts ==============================
platform linux -- Python 3.12.8, pytest-8.3.4, pluggy-1.5.0
rootdir: /home/runner/work/py-techguides/py-techguides/codebase
configfile: pyproject.toml
plugins: anyio-4.8.0
collecting ... collected 23 items                                                             

tests/library/test_book_service.py::test_add_book_with_existing_data PASSED [  4%]
tests/library/test_book_service.py::test_add_book_with_missing_data[missing_fields0] PASSED [  8%]
tests/library/test_book_service.py::test_add_book_with_missing_data[missing_fields1] PASSED [ 13%]
tests/library/test_book_service.py::test_add_book_with_missing_data[missing_fields2] PASSED [ 17%]
tests/library/test_book_service.py::test_get_book_with_existing_id PASSED [ 21%]
tests/library/test_book_service.py::test_get_book_with_missing_id PASSED [ 26%]
tests/library/test_book_service.py::test_book_is_available_with_existing_id PASSED [ 30%]
tests/library/test_book_service.py::test_book_is_available_with_borrowed_book PASSED [ 34%]
tests/library/test_book_service.py::test_book_is_available_with_missing_id PASSED [ 39%]
tests/library/test_book_service.py::test_borrow_book_with_existing_id PASSED [ 43%]
tests/library/test_book_service.py::test_borrow_book_with_missing_id PASSED [ 47%]
tests/library/test_book_service.py::test_borrow_book_unavailable PASSED  [ 52%]
tests/library/test_book_service.py::test_search_book_by_full_title PASSED [ 56%]
tests/library/test_book_service.py::test_search_book_by_partial_title PASSED [ 60%]
tests/library/test_book_service.py::test_search_book_by_title_missing PASSED [ 65%]
tests/library/test_book_service.py::test_search_book_by_title_empty PASSED [ 69%]
tests/library/test_book_service.py::test_return_book_borrowed PASSED     [ 73%]
tests/library/test_book_service.py::test_return_book_not_borrowed PASSED [ 78%]
tests/library/test_book_service.py::test_return_book_with_missing_id PASSED [ 82%]
tests/library/test_book_service.py::test_return_book_already_returned PASSED [ 86%]
tests/library/test_book_service.py::test_remove_book_with_existing_id PASSED [ 91%]
tests/library/test_book_service.py::test_remove_book_with_missing_id PASSED [ 95%]
tests/library/test_book_service.py::test_remove_book_with_borrowed_id PASSED [100%]

============================== 23 passed in 0.04s ==============================
User Service Implementation

We implement a library’s user service similarly, by moving from the abstract class to a concrete implementation:

# src/python_training_material/library/user_service.py
from abc import ABC, abstractmethod

from python_training_material.library.book_service import AbstractBookService


class AbstractUserService(ABC):

    @abstractmethod
    def add_user(self, name: str) -> int | None:
        pass

    @abstractmethod
    def remove_user(self, user_id: int) -> bool:
        pass

    @abstractmethod
    def borrow_book(self, user_id: int, book_id: int) -> bool:
        pass

    @abstractmethod
    def return_book(self, user_id: int, book_id: int) -> bool:
        pass


class UserService(AbstractUserService):

    def __init__(self, book_service: AbstractBookService):
        self._users = {}
        self._id_counter = 1
        self._book_service = book_service

    def add_user(self, name: str) -> int | None:
        user_id = self._id_counter
        if name == "":
            return None
        user: dict[str, list[int]] = {"name": name, "borrowed_books_ids": []}
        self._users[user_id] = user
        self._id_counter += 1
        return user_id

    def _user_exists(self, user_id: int) -> bool:
        return user_id in self._users

    def get_user(self, user_id: int) -> dict | None:
        if not self._user_exists(user_id):
            return None
        return self._users[user_id]

    def borrow_book(self, user_id: int, book_id: int) -> bool:
        if not self._user_exists(user_id):
            return False
        book_borrowed = self._book_service.borrow_book(book_id)
        if book_borrowed:
            self._users[user_id]["borrowed_books_ids"].append(book_id)
            return True  # Indicate successful borrow
        return False

    def user_borrowed_book(self, user_id: int, book_id: int) -> bool:
        if not self._user_exists(user_id):
            return False
        return book_id in self.get_user(user_id)["borrowed_books_ids"]

    def return_book(self, user_id: int, book_id: int) -> bool:
        if not self._user_exists(user_id):
            return False
        if book_id not in self._users[user_id]["borrowed_books_ids"]:
            return False
        book_returned = self._book_service.return_book(book_id)
        if book_returned:
            self._users[user_id]["borrowed_books_ids"].remove(book_id)
            return True  # Indicate successful return
        return False

    def remove_user(self, user_id: int) -> bool:
        if self._user_exists(user_id):
            # Check if the user has borrowed books, if so, cannot be removed
            if self._users[user_id]["borrowed_books_ids"]:
                return False
            del self._users[user_id]
            return True  # Indicate successful deletion
        return False

We then define a series of unit tests encompassing all its methods. Note how the UserService class relies on BookService to determine whether a book is available and can be borrowed/returned.
The following unit tests verify the behavior of the implemented UserService in isolation from the concrete BookService implementation, relying on mocking the behavior of the (abstract) BookService class.

# tests/library/test_user_service.py
from unittest.mock import create_autospec

import pytest
from python_training_material.library.book_service import AbstractBookService
from python_training_material.library.user_service import UserService


@pytest.fixture
def mocked_book_service():
    """
    Fixture to provide a mocked instance of AbstractBookService using create_autospec.
    """
    mock = create_autospec(AbstractBookService, instance=True)
    return mock


@pytest.fixture
def user_service(mocked_book_service):
    """
    Fixture to provide a fresh instance of UserService with a mocked BookService.
    """
    return UserService(book_service=mocked_book_service)


@pytest.fixture
def user_data():
    """
    Fixture to provide user data for testing.
    """
    return {"name": "Davide Vitiello"}


@pytest.fixture
def created_user_id(user_service, user_data):
    """
    Fixture to create a user and returning its id.
    """
    user_id = user_service.add_user(**user_data)
    return user_id


def test_add_user_with_existing_name(user_service, user_data):
    """
    Test that a user can be registered successfully.
    """
    user_id = user_service.add_user(**user_data)
    assert user_service.get_user(user_id) == user_data | {"borrowed_books_ids": []}


def test_add_user_with_missing_name(user_service):
    """
    Test that adding a user with a missing name returns None.
    """
    assert user_service.add_user("") is None


def test_get_user_with_existing_id(user_service, user_data, created_user_id):
    """
    Test that a user can be retrieved successfully.
    """
    assert user_service.get_user(created_user_id) == user_data | {
        "borrowed_books_ids": []
    }


def test_get_user_with_missing_id(user_service, missing_id=999):
    """
    Test that a non-existent user returns None.
    """
    # Assuming the value of `missing_id` does not represent an existing user
    assert user_service.get_user(missing_id) is None


def test_borrow_book_with_existing_user(
    user_service, mocked_book_service, created_user_id
):
    """
    Test that a user can borrow a book successfully.
    """
    book_id = 1
    mocked_book_service.borrow_book.return_value = True

    assert user_service.borrow_book(created_user_id, book_id) is True

    # Assert that borrow_book was called with the correct book_id
    mocked_book_service.borrow_book.assert_called_once_with(book_id)

    # Verify that the book is in the user's `borrowed_books_ids`
    assert user_service.user_borrowed_book(created_user_id, book_id) is True


def test_borrow_book_with_missing_user(
    user_service, mocked_book_service, missing_id=999
):
    """
    Test that borrowing a book with a non-existent user returns False.
    """
    book_id = 1

    # Assuming the value of `missing_id` does not represent an existing user
    assert user_service.borrow_book(missing_id, book_id) is False

    # Assert that borrow_book was not called since the user does not exist
    mocked_book_service.borrow_book.assert_not_called()

    # Verify that the book is not in the user's `borrowed_books_ids`
    assert user_service.user_borrowed_book(missing_id, book_id) is False


def test_return_book_with_existing_user(
    user_service, mocked_book_service, created_user_id
):
    """
    Test that a user can return a book successfully.
    """
    book_id = 1
    mocked_book_service.borrow_book.return_value = True
    mocked_book_service.return_book.return_value = True

    # Borrow the book
    user_service.borrow_book(created_user_id, book_id)

    # Return the book
    assert user_service.return_book(created_user_id, book_id) is True

    # Assert that return_book was called with the correct book_id
    mocked_book_service.return_book.assert_called_once_with(book_id)

    # Verify that the book is no longer in the user's borrowed_books
    assert user_service.user_borrowed_book(created_user_id, book_id) is False


def test_return_book_with_missing_user(
    user_service, mocked_book_service, missing_id=999
):
    """
    Test that returning a book with a non-existent user returns False.
    """
    book_id = 1

    # Assuming the value of `missing_id` does not represent an existing user
    assert user_service.return_book(missing_id, book_id) is False

    # Assert that return_book was not called since the user does not exist
    mocked_book_service.return_book.assert_not_called()


def test_return_book_not_borrowed(user_service, mocked_book_service, created_user_id):
    """
    Test that returning a book that was not borrowed by the specified user returns False.
    """
    book_id = 1
    mocked_book_service.borrow_book.return_value = True

    # First, add a user and borrow a book
    borrowing_user_id = user_service.add_user("Average Joe")
    user_service.borrow_book(borrowing_user_id, book_id)

    # Attempt to return the same book for a different user
    assert user_service.return_book(created_user_id, book_id) is False

    # Assert that return_book was NOT called since the user did not borrow the book
    mocked_book_service.return_book.assert_not_called()


def test_remove_user_with_existing_id(user_service, created_user_id):
    """
    Test that a user can be removed successfully.
    """
    assert user_service.remove_user(created_user_id) is True
    assert user_service.get_user(created_user_id) is None


def test_remove_user_with_missing_id(user_service, mocked_book_service, missing_id=999):
    """
    Test that removing a user with a non-existent id returns False.
    """
    # Assuming the value of `missing_id` does not represent an existing user
    assert user_service.remove_user(missing_id) is False
    assert user_service.get_user(missing_id) is None

    # Assert that no interaction with BookService occurred
    mocked_book_service.assert_not_called()
============================= test session starts ==============================
platform linux -- Python 3.12.8, pytest-8.3.4, pluggy-1.5.0
rootdir: /home/runner/work/py-techguides/py-techguides/codebase
configfile: pyproject.toml
plugins: anyio-4.8.0
collecting ... collected 11 items                                                             

tests/library/test_user_service.py::test_add_user_with_existing_name PASSED [  9%]
tests/library/test_user_service.py::test_add_user_with_missing_name PASSED [ 18%]
tests/library/test_user_service.py::test_get_user_with_existing_id PASSED [ 27%]
tests/library/test_user_service.py::test_get_user_with_missing_id PASSED [ 36%]
tests/library/test_user_service.py::test_borrow_book_with_existing_user PASSED [ 45%]
tests/library/test_user_service.py::test_borrow_book_with_missing_user PASSED [ 54%]
tests/library/test_user_service.py::test_return_book_with_existing_user PASSED [ 63%]
tests/library/test_user_service.py::test_return_book_with_missing_user PASSED [ 72%]
tests/library/test_user_service.py::test_return_book_not_borrowed PASSED [ 81%]
tests/library/test_user_service.py::test_remove_user_with_existing_id PASSED [ 90%]
tests/library/test_user_service.py::test_remove_user_with_missing_id PASSED [100%]

============================== 11 passed in 0.08s ==============================

Integration Tests

Integration testing is a crucial aspect of developing a Python-based codebase, ensuring that different parts of your application work together seamlessly. With pytest, writing and running integration tests becomes a more manageable and streamlined process, contributing significantly to the overall quality of your codebase. Here, we’ll explore how to implement effective integration tests with pytest.

In this integration test, the UserService and BookService classes must interact to model a user borrowing and returning a book from a library. The library has to update its list of available books every time a user borrows some. Given the interaction of the book and user services as part of the borrowing and returning of books, we can model this scenario as one or more integration tests.

In the following, we check the sanity of the interaction between the UserService and BookService classes in different scenarios, as for example, a user borrowing a book and returning it:

# tests/library/test_integration_book_user_services.py
import pytest
from python_training_material.library.book_service import BookService
from python_training_material.library.user_service import UserService


@pytest.fixture
def book_service():
    return BookService()


@pytest.fixture
def user_service(book_service):
    return UserService(book_service)


@pytest.fixture
def created_user_id(user_service):
    return user_service.add_user("Davide Vitiello")


@pytest.fixture
def created_book_id(book_service):
    return book_service.add_book("1984", "George Orwell")


def test_user_with_existing_id_borrows_book(
    user_service, book_service, created_user_id, created_book_id
):
    """
    Test to verify that a user can borrow a book,
    and BookService and UserService states are updated accordingly.
    """
    # Borrow the book
    assert user_service.borrow_book(created_user_id, created_book_id) is True

    # Verify that the user has borrowed the book
    assert user_service.user_borrowed_book(created_user_id, created_book_id) is True

    # Verify that the book is not available
    assert book_service.book_is_available(created_book_id) is False


def test_user_with_existing_id_borrows_missing_book(
    user_service, created_user_id, missing_book_id=999
):
    """
    Test to verify that a user cannot borrow a book if the book does not exist.
    """
    # Assuming the value of `missing_book_id` does not represent an existing book
    assert user_service.borrow_book(created_user_id, missing_book_id) is False


def test_user_with_missing_id_borrows_book(
    user_service, book_service, created_book_id, missing_user_id=999
):
    """
    Test to verify that a user cannot borrow a book if the user does not exist.
    """
    # Assuming the value of `missing_user_id` does not represent an existing user
    assert user_service.borrow_book(missing_user_id, created_book_id) is False
    assert book_service.book_is_available(created_book_id) is True


def test_user_with_existing_id_returns_book(
    user_service, book_service, created_user_id, created_book_id
):
    """
    Test to verify that a user can return a borrowed book
    and that both services' states are updated accordingly.
    """
    # Borrow the book
    user_service.borrow_book(created_user_id, created_book_id)

    # Return the book
    assert user_service.return_book(created_user_id, created_book_id) is True

    # Verify the user no longer has the book
    assert user_service.user_borrowed_book(created_user_id, created_book_id) is False

    # Verify that BookService marks the returned book available again
    assert book_service.book_is_available(created_book_id) is True


def test_user_with_existing_id_returns_book_not_borrowed(
    user_service, book_service, created_user_id
):
    """
    Test to verify that a user cannot return a book if the user did not borrow it.
    """
    # Add a new book
    book_id = book_service.add_book("Pride and Prejudice", "Jane Austen")

    # Try to return the book
    assert user_service.return_book(created_user_id, book_id) is False


def test_user_with_missing_id_returns_book(
    user_service, created_book_id, missing_user_id=999
):
    """
    Test to verify that a user cannot return a book if the user does not exist.
    """
    # Assuming the value of `missing_user_id` does not represent an existing user
    assert user_service.return_book(missing_user_id, created_book_id) is False


def test_remove_borrowed_book(
    user_service, book_service, created_user_id, created_book_id
):
    """
    Test to verify that borrowed book cannot be removed.
    """
    user_service.borrow_book(created_user_id, created_book_id)
    assert book_service.remove_book(created_book_id) is False


def test_remove_user_with_borrowed_book(user_service, created_user_id, created_book_id):
    """
    Test to verify that user with borrowed book cannot be removed.
    """
    user_service.borrow_book(created_user_id, created_book_id)
    assert user_service.remove_user(created_user_id) is False
============================= test session starts ==============================
platform linux -- Python 3.12.8, pytest-8.3.4, pluggy-1.5.0
rootdir: /home/runner/work/py-techguides/py-techguides/codebase
configfile: pyproject.toml
plugins: anyio-4.8.0
collecting ... collected 8 items                                                              

tests/library/test_integration_user_book_service.py::test_user_with_existing_id_borrows_book PASSED [ 12%]
tests/library/test_integration_user_book_service.py::test_user_with_existing_id_borrows_missing_book PASSED [ 25%]
tests/library/test_integration_user_book_service.py::test_user_with_missing_id_borrows_book PASSED [ 37%]
tests/library/test_integration_user_book_service.py::test_user_with_existing_id_returns_book PASSED [ 50%]
tests/library/test_integration_user_book_service.py::test_user_with_existing_id_returns_book_not_borrowed PASSED [ 62%]
tests/library/test_integration_user_book_service.py::test_user_with_missing_id_returns_book PASSED [ 75%]
tests/library/test_integration_user_book_service.py::test_remove_borrowed_book PASSED [ 87%]
tests/library/test_integration_user_book_service.py::test_remove_user_with_borrowed_book PASSED [100%]

============================== 8 passed in 0.03s ===============================
Back to top