π¦ Packaging, Type Hints & Testing in Python: Let's Learn from ZERO!
If you're building a FastAPI + PostgreSQL backend (like a social-feed app), these topics are not random. They work together to make your project:
- π» Easy to install and run on another computer
- π Organized with the correct dependencies
- π‘οΈ Safer through type checking
- β Reliable through automated testing
- π§Ή Easier to maintain as it grows
We'll cover the meaning, why we need each tool, how it works, commands, code examples, and how it applies to a real backend. π
πΊοΈ The Big Picture
- π¦ Packaging & Dependency Management β prepare the project and its libraries so it runs consistently
- π·οΈ Type Hints & Static Checking β say what types your functions expect, and catch mistakes before running
- π§ͺ Automated Testing with pytest β check your code behaves correctly in different situations
π¦ PART 1: Virtual Environments (venv / Poetry)
1. π€ What is a Python dependency?
Your social-feed backend needs things like:
- β‘ FastAPI: to create APIs
- π¦ Uvicorn: to run the app
- ποΈ SQLAlchemy: to talk to the database
- βοΈ Pydantic: to validate data
- π Alembic: for database migrations
- π§ͺ pytest: to test your code
You didn't write these yourself. You install and use them.
π A dependency is an external package or library that your project needs in order to work.
from fastapi import FastAPI
app = FastAPI()
Here, FastAPI is a dependency.
2. π What problem do virtual environments solve?
Imagine two projects:
| Project | FastAPI | SQLAlchemy | Pydantic |
|---|---|---|---|
| π °οΈ Social Feed | 0.115 | 2.x | 2.x |
| π ±οΈ Old College Project | 0.95 | 1.4 | 1.x |
Both need FastAPI, but different versions. If you install everything globally, installing a version for Project B can break Project A. π₯
A virtual environment is an isolated Python environment created for one project. Think of separate rooms in a house π : each project has its own room and its own libraries, so they never interfere.
3. π οΈ What is venv?
venv is Python's built-in module for creating virtual environments. No separate install needed.
Step 1: create a project folder
mkdir social-feed
cd social-feed
Step 2: create the virtual environment
python -m venv .venv
| Part | Meaning |
|---|---|
python |
Run Python |
-m |
Execute a Python module |
venv |
Use the virtual environment module |
.venv |
Name of the environment folder |
Step 3: activate it (tells your terminal to use this environment's Python and packages)
Windows PowerShell:
.venv\Scripts\Activate.ps1
Windows Command Prompt:
.venv\Scripts\activate.bat
Linux / macOS:
source .venv/bin/activate
You'll see (.venv) in your terminal prompt. β
That means it's active.
Step 4: install dependencies
pip install fastapi uvicorn sqlalchemy pytest
pip is Python's package installer. It installs into the currently active environment.
Step 5: check what's installed
pip list
Step 6: deactivate
deactivate
4. π pip freeze and requirements.txt
Your teammate downloads your project from GitHub. Will they automatically have your packages? No! Source code and installed libraries are separate things. You must tell them which packages and versions to install.
pip freeze
Example output:
fastapi==0.115.0
uvicorn==0.30.6
sqlalchemy==2.0.35
pytest==8.3.3
Save it to a file:
pip freeze > requirements.txt
Your project now looks like:
social-feed/
β
βββ .venv/
βββ app/
βββ requirements.txt
βββ main.py
Your teammate recreates the environment:
python -m venv .venv
# activate it, then:
pip install -r requirements.txt
-r means read the requirements from a file.
β οΈ Important: Do not commit your
.venvfolder to GitHub. It's machine-specific and can be recreated. Add.venv/to.gitignore.
5. π What is Poetry?
-
venvβ creates isolated environments -
pipβ installs packages -
requirements.txtβ records dependencies
Managing a big project with several separate tools and files gets messy. Poetry is a Python dependency and packaging tool that brings many of these tasks together. It can:
- π Manage dependencies
- π Create and manage virtual environments
- π Lock exact dependency versions
- π¦ Build Python packages
- π Help publish packages
π§° How do we use Poetry?
Install Poetry using its official installation instructions, then inside your project:
poetry init # creates pyproject.toml interactively
poetry new myproject # or start a brand-new project
poetry add fastapi sqlalchemy # add dependencies
poetry add --group dev pytest mypy # add development-only dependencies
poetry install # install everything defined in the project
poetry run python main.py # run a command inside Poetry's environment
π Depending on your Poetry version and installed plugins,
poetry shellmay not be available.poetry run ...always works.
π What is poetry.lock?
When you install FastAPI, FastAPI itself depends on other packages. These are called transitive dependencies:
Your Project
β
βββ FastAPI
β
βββ Starlette
βββ Other dependencies
poetry.lock records the exact resolved versions of everything, including transitive dependencies, so everyone installs the same set. π―
6. βοΈ venv vs pip vs Poetry
| Tool | Main responsibility |
|---|---|
venv |
Creates isolated environments |
pip |
Installs Python packages |
requirements.txt |
Lists dependencies |
| Poetry | Manages dependencies, environments and packaging |
pyproject.toml |
Stores project configuration and metadata |
poetry.lock |
Locks resolved dependency versions |
π Poetry does not replace Python. It manages your project's packaging and dependencies. You can also use
venv+pipwithout Poetry.
βοΈ PART 2: pyproject.toml & Dependency Management
7. π What exactly is pyproject.toml?
One of the most important files in modern Python projects. It's a configuration file used by packaging tools and other development tools, written in TOML (Tom's Obvious Minimal Language, just a human-readable config format).
name = "social-feed"
version = "1.0.0"
description = "A social media backend"
π€· Why do we need it?
Your project may use Poetry (dependencies), Ruff (linting), mypy (type checking), pytest (testing), and a build backend. Instead of scattering configs across many files, modern projects centralize much of it in pyproject.toml.
ποΈ Example structure
social-feed/
β
βββ pyproject.toml
βββ poetry.lock
βββ README.md
βββ app/
β βββ main.py
β βββ models/
β βββ services/
β βββ repositories/
β
βββ tests/
π Example pyproject.toml (Poetry format)
[tool.poetry]
name = "social-feed"
version = "1.0.0"
description = "Social Feed Backend"
authors = ["Developer"]
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.115"
sqlalchemy = "^2.0"
uvicorn = "^0.30"
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
mypy = "^1.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
| Section | Purpose |
|---|---|
[tool.poetry] |
Project metadata |
[tool.poetry.dependencies] |
Main dependencies |
[tool.poetry.group.dev.dependencies] |
Development dependencies |
[build-system] |
Defines how the project is built |
π This is the Poetry-specific format. Modern Poetry versions also support other configuration options.
8. π Runtime vs development dependencies
Imagine your backend is deployed to a cloud host. Does production need pytest to serve an API request? No. Does it need FastAPI? Yes. So we split dependencies in two:
π Runtime dependencies (needed when the app runs)
- FastAPI, SQLAlchemy, Uvicorn, Pydantic
π οΈ Development dependencies (needed while developing, checking or testing)
- pytest, mypy, Ruff, pytest-cov
Why separate them? Production should contain only what it needs to run, where practical. π§Ή
9. π’ What is semantic versioning?
Versions look like 2.4.1, which means MAJOR.MINOR.PATCH:
| Part | Meaning |
|---|---|
2 |
Major: may introduce breaking changes |
4 |
Minor: adds backward-compatible features |
1 |
Patch: fixes bugs |
2.4.1 β 2.4.2 patch update π©Ή
2.4.1 β 2.5.0 minor update β¨
2.4.1 β 3.0.0 major update π₯
π― What do version constraints mean?
fastapi = "^0.115"
The caret ^ allows compatible versions according to Poetry's rules:
-
^0.115allows0.115.0up to, but not including,0.116.0 -
^2.0allows2.0.0up to, but not including,3.0.0
π·οΈ PART 3: Type Hints in Python
10. π€ What are type hints?
def add(a, b):
return a + b
What types should a and b be? Integers? Strings? Lists? Python doesn't say:
print(add(10, 20)) # 30
print(add("Hello ", "Deepika")) # Hello Deepika
Both work! Python is dynamically typed: it figures out types at runtime. But in a big backend we want our code to communicate what it expects.
π Type hints are annotations that tell developers and static analysis tools what type a variable, parameter, or return value is expected to have.
Without type hints:
def greet(name):
return "Hello " + name
With type hints:
def greet(name: str) -> str:
return "Hello " + name
| Syntax | Meaning |
|---|---|
name |
Parameter name |
: str |
Expected parameter type is string |
-> str |
Expected return type is string |
π¨ Python does NOT enforce type hints at runtime!
A very common beginner misconception:
def greet(name: str) -> str:
return "Hello " + name
print(greet(100))
Python still lets you call it, but raises a TypeError when it tries to join a string and an integer. Type hints are not runtime validation. They help tools and developers detect mistakes.
11. π Common types used in type hints
| Python type | Represents | Example |
|---|---|---|
int |
Integer | 25 |
float |
Decimal number | 9.5 |
str |
Text | "Deepika" |
bool |
True or false | True |
list |
Collection of values | [1, 2, 3] |
dict |
Key-value pairs | {"name": "Deepika"} |
tuple |
Ordered fixed collection | (10, 20) |
set |
Unique values | {1, 2, 3} |
def calculate_age(age: int) -> int:
return age + 1
def get_username(name: str) -> str:
return name.upper()
def is_adult(age: int) -> bool:
return age >= 18
π³οΈ A function that returns nothing
def display_message(message: str) -> None:
print(message)
None means the function doesn't return a meaningful value. It performs an action.
result = display_message("Hello")
print(result)
Hello
None
12. π§Ί Type hints for collections
def display_users(users: list[str]) -> None:
for user in users:
print(user)
display_users(["Deepika", "Jeeva", "Rahul"])
-
listβ it's a list -
strβ every element is expected to be a string
def calculate_total(numbers: list[int]) -> int:
return sum(numbers)
π Dictionaries
def get_user() -> dict[str, str]:
return {
"name": "Deepika",
"username": "deepika123",
}
dict[str, str] means keys are strings and values are strings. For mixed values:
def get_profile() -> dict[str, int | str]:
return {
"username": "deepika123",
"age": 21,
}
The | symbol means either type.
13. π Union types
def get_user_id(user_id: int | str) -> str:
return str(user_id)
get_user_id(101)
get_user_id("USER101")
Both are valid. Before Python 3.10 you'd see:
from typing import Union
def get_user_id(user_id: Union[int, str]) -> str:
return str(user_id)
Same idea, older syntax.
14. β Optional types
Very useful in backend work. A user may or may not have a profile picture:
def get_profile_picture() -> str | None:
return "https://example.com/profile.jpg"
It can return a string URL, or None if there's no picture.
image_url = get_profile_picture()
if image_url is not None:
print(image_url)
else:
print("No profile picture")
Before Python 3.10:
from typing import Optional
def get_profile_picture() -> Optional[str]:
return None
β οΈ
Optional[str]meansstr | None. Optional does not mean the parameter can be omitted. It means the value can beNone.
15. β‘ Type hints in your FastAPI project
Without type hints:
def get_post(post_id):
post = repository.find(post_id)
return post
With type hints:
def get_post(post_id: int) -> dict | None:
post = repository.find(post_id)
return post
Now anyone reading knows post_id is an integer and the function returns a dictionary or None. In real apps you'd use SQLAlchemy models or Pydantic schemas instead of a generic dict:
def get_post(post_id: int) -> Post | None:
return repository.get_by_id(post_id)
16. π What is static type checking?
def calculate_total(price: int, quantity: int) -> int:
return price * quantity
total = calculate_total("100", 5) # π¬ string instead of int
Python may not complain until execution reaches an operation that can't handle those values. Can we catch it before running? Yes, with static type checking: analyzing your source code without executing it. Think of a teacher checking your answer sheet before you submit. π
17. π§ What is mypy?
mypy is a static type checker for Python. It checks whether your code agrees with its type annotations.
pip install mypy
Create example.py:
def add(a: int, b: int) -> int:
return a + b
result = add("10", 20)
mypy example.py
mypy reports an error because "10" is a string but the function expects an integer. β Fix it with add(10, 20), run mypy again, and it passes. β
What mypy helps detect:
- Passing a string where an int is expected
- Returning the wrong type from a function
- Accessing attributes a type doesn't have
- Incorrect function argument types
- Using
Nonewhere a non-optional value is expected
π Does mypy execute your code?
No!
| Python execution | mypy |
|---|---|
| Executes your program | Analyzes your source code |
| Finds runtime errors when they occur | Finds potential type errors before execution |
| Performs actual operations | Does not run your application |
βοΈ Configure mypy in pyproject.toml
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
mypy app/
β οΈ
strict = trueenables a collection of stricter checks. It may reveal many issues in an existing project, so you can adopt stricter checking gradually.
π§ͺ PART 4: Testing with pytest
18. π€ Why do we need testing?
Your social media backend has: registration, login, create post, like post, comment, follow, and fetch home feed. You test everything manually and it works. Then your mentor asks you to change the like feature. You change it, but accidentally comments stop working. You might not notice until someone tests manually. π±
π Testing means checking whether your code produces the expected result under specific conditions. The goal is to catch bugs early and stop previously working features from breaking.
19. π€ What is automated testing?
Instead of re-checking everything by hand, we write code that checks our code:
def add(a, b):
return a + b
We can check that add(10, 20) == 30. If correct, the test passes β
. If not, it fails β.
20. π What is pytest?
pytest is a popular Python testing framework that helps you write and run automated tests.
pip install pytest
A simple project:
myproject/
β
βββ calculator.py
βββ test_calculator.py
calculator.py:
def add(a: int, b: int) -> int:
return a + b
def subtract(a: int, b: int) -> int:
return a - b
test_calculator.py:
from calculator import add, subtract
def test_add():
assert add(10, 20) == 30
def test_subtract():
assert subtract(20, 10) == 10
βοΈ What is assert?
assert checks whether a condition is true.
assert 10 + 20 == 30 # true, nothing happens
assert 10 + 20 == 50 # false, raises AssertionError
In pytest, assertions verify expected results.
βΆοΈ How to run tests
From your project root:
pytest
================ test session starts ================
test_calculator.py .. [100%]
================ 2 passed ===========================
Two dots = two tests passed. π If a test fails, pytest shows the failure and explains the mismatch.
π Why do test files start with test_?
pytest automatically discovers files matching test_*.py or *_test.py, and functions starting with test_:
def test_login():
...
No need to register tests manually. β¨
21. πͺ Types of testing
| Type | What it tests | Example |
|---|---|---|
| π¬ Unit | One small unit (a function or service method) | Does password validation work? |
| π Integration | Multiple components working together | Does the API talk correctly to PostgreSQL? |
| π End-to-End (E2E) | A complete user workflow | Register β log in β create post β see it in feed |
A unit test usually isolates the function being tested. An integration test deliberately connects multiple real components. pytest can be used for all of these.
π§° PART 5: pytest Fixtures
22. π€ What is a fixture?
Imagine 10 tests that each need a user object. Without fixtures:
def test_user_name():
user = {"id": 1, "name": "Deepika"}
assert user["name"] == "Deepika"
def test_user_id():
user = {"id": 1, "name": "Deepika"}
assert user["id"] == 1
We're creating the same test data again and again. π©
π A fixture is a function that prepares and provides reusable data or resources to test functions. Think of it as a person who sets everything up before an experiment begins. π§βπ¬
Step 1: create a fixture
import pytest
@pytest.fixture
def sample_user():
return {
"id": 1,
"name": "Deepika",
"username": "deepika123",
}
@pytest.fixture tells pytest: "This function provides a fixture other tests can request."
Step 2: use it
def test_user_name(sample_user):
assert sample_user["name"] == "Deepika"
def test_user_id(sample_user):
assert sample_user["id"] == 1
We never called sample_user(). How does it work? This is pytest's fixture injection: when a test parameter's name matches a fixture, pytest automatically runs the fixture and passes its returned value into the test.
@pytest.fixture
sample_user() β pytest executes the fixture
β
βΌ
test_user_name(sample_user) β receives the returned dictionary
23. π― Fixture scopes
Should pytest recreate the same resource before every test? Sometimes yes, sometimes no.
| Scope | When the fixture is created |
|---|---|
function |
Once for each test function (default) |
class |
Once for each test class |
module |
Once for each Python test module |
package |
Once for a package |
session |
Once for the entire test session |
@pytest.fixture(scope="function")
def sample_user():
return {"name": "Deepika"}
@pytest.fixture(scope="session")
def app_config():
return {"environment": "test"}
β οΈ A wider scope can improve performance, but it can cause tests to share mutable state. Use it carefully.
24. π§Ή yield in fixtures (setup + cleanup)
Testing database code, we need to: open a connection β run the test β close the connection. Use yield:
@pytest.fixture
def database_connection():
print("Opening database")
connection = "Database Connection"
yield connection
print("Closing database")
-
Before
yieldβ setup -
At
yieldβ provide the resource to the test -
After
yieldβ cleanup
def test_database(database_connection):
assert database_connection == "Database Connection"
The cleanup code runs after the test finishes, even if the test fails. For real databases, you'd return an actual connection/session and close or roll it back in the teardown section.
π PART 6: pytest Parametrize
25. π€ What is parametrize?
def is_eligible(age: int) -> bool:
return age >= 18
Test ages 15 β False, 17 β False, 18 β True, 21 β True, 25 β True. Five separate test functions would be repetitive. parametrize runs the same test with different inputs:
import pytest
@pytest.mark.parametrize(
"age, expected",
[
(15, False),
(17, False),
(18, True),
(21, True),
(25, True),
],
)
def test_voting_eligibility(age, expected):
assert is_eligible(age) == expected
-
@pytest.mark.parametrize(β run this test with multiple sets of inputs -
"age, expected"β the parameter names - the list of tuples β the test cases, where each tuple = one test execution
Test 1: age = 15 β expected False
Test 2: age = 17 β expected False
Test 3: age = 18 β expected True
Test 4: age = 21 β expected True
Test 5: age = 25 β expected True
5 input combinations β 5 test executions β
π Useful for your backend
Test a valid username, an empty username, a username with spaces, a very long username, and a duplicate username, all with one parametrized test.
π§ Fixture β provides reusable test data/resources. Parametrize β runs the same test with different inputs.
π PART 7: Monkeypatch in pytest
26. π€ The problem monkeypatch solves
Your backend sends an email after registration:
def send_email(email: str) -> str:
print(f"Sending email to {email}")
return "Email sent"
When testing registration, you don't want to send a real email every time because:
- π§ It may send actual emails
- π It may need internet access
- π It may need credentials
- π It makes tests slower
- πΈ It may cost money
So we temporarily replace the real function with a fake one.
π monkeypatch is a pytest feature that temporarily changes an object's attributes, functions, environment variables, or other values during a test. After the test, pytest automatically restores the original state.
27. π οΈ Simple monkeypatch example
email_service.py:
def send_email(email: str) -> str:
return f"Real email sent to {email}"
registration.py:
from email_service import send_email
def register_user(email: str) -> str:
result = send_email(email)
return result
test_registration.py:
import registration
def test_register_user(monkeypatch):
def fake_send_email(email: str) -> str:
return "Fake email sent"
monkeypatch.setattr(
"registration.send_email",
fake_send_email,
)
result = registration.register_user("test@example.com")
assert result == "Fake email sent"
The key line:
monkeypatch.setattr("registration.send_email", fake_send_email)
It says: "Temporarily replace the send_email used by the registration module with my fake function." No real email is sent. π
28. π§° Important monkeypatch methods
A. Set an attribute
monkeypatch.setattr(object, "attribute", new_value)
B. Set an environment variable
monkeypatch.setenv("APP_ENV", "test")
C. Delete an environment variable
monkeypatch.delenv("DATABASE_URL", raising=False)
D. Change a dictionary value
config = {"debug": False}
monkeypatch.setitem(config, "debug", True)
E. Change the working directory
monkeypatch.chdir("/tmp")
π‘ Don't memorize every method. Understand the core idea: temporarily change something so you can control a test.
π PART 8: Mocking with unittest.mock
29. π€ What is mocking?
Your registration workflow:
User Registration
β
βΌ
Validate User
β
βΌ
Save User in Database
β
βΌ
Send Welcome Email
β
βΌ
Return Success
To test registration, you don't want to connect to a real database, send a real email, call a payment API, or contact a third-party service. You want to isolate your registration logic.
π Mocking replaces real dependencies with controlled test doubles. These fake objects let you control responses and check how your code interacts with them.
30. π¦ What is unittest.mock?
A built-in Python library (no install needed) with tools like Mock, MagicMock, patch, call_args, and assert_called_once_with().
31. π What is Mock?
A Mock is a fake object that can imitate another object.
from unittest.mock import Mock
email_sender = Mock()
email_sender.send.return_value = "Email sent"
result = email_sender.send("test@example.com")
print(result)
Email sent
-
Mock()creates a fake object -
.send.return_value = "Email sent"tells the fakesend()what to return -
email_sender.send(...)calls the fake method. No real email is sent.
32. π return_value
from unittest.mock import Mock
database = Mock()
database.get_user.return_value = {"id": 1, "name": "Deepika"}
user = database.get_user(1)
print(user)
{'id': 1, 'name': 'Deepika'}
You simulate database responses without a database. ποΈ
33. π₯ side_effect
What if your database connection fails? Use side_effect to raise an exception:
from unittest.mock import Mock
database = Mock()
database.get_user.side_effect = Exception("Database error")
database.get_user(1) # raises Exception: Database error
It can also return different values on successive calls:
service = Mock()
service.get_data.side_effect = [10, 20, 30]
print(service.get_data()) # 10
print(service.get_data()) # 20
print(service.get_data()) # 30
| Feature | Purpose |
|---|---|
return_value |
Define what a mock returns |
side_effect |
Define behavior such as exceptions or different successive results |
34. π©Ή What is patch?
Probably the most important part of unittest.mock. patch temporarily replaces an existing object with a mock.
payment_service.py:
def process_payment(amount: int) -> str:
return "Real payment processed"
order.py:
from payment_service import process_payment
def place_order(amount: int) -> str:
result = process_payment(amount)
return result
test_order.py:
from unittest.mock import patch
import order
@patch("order.process_payment")
def test_place_order(mock_payment):
mock_payment.return_value = "Payment successful"
result = order.place_order(500)
assert result == "Payment successful"
-
@patch("order.process_payment")temporarily replacesprocess_paymentin theordermodule -
mock_payment.return_value = ...configures what the mock returns -
order.place_order(500)runs the real order function, but its payment dependency is fake
π¨ Extremely important: patch where the object is used!
Why @patch("order.process_payment") and not @patch("payment_service.process_payment")?
Because order.py did this:
from payment_service import process_payment
That created a name process_payment inside the order module, and place_order() uses that name. So we patch the name where the code under test looks it up. This is one of the most common mocking mistakes! β οΈ
35. β
assert_called_once_with
Verify your app actually called the payment function with the right amount:
mock_payment.assert_called_once_with(500)
This checks that the mock was called exactly once and with argument 500.
| Method | What it checks |
|---|---|
assert_called() |
Called at least once |
assert_not_called() |
Never called |
assert_called_once() |
Called exactly once |
assert_called_with(500) |
Most recent call used argument 500 |
assert_called_once_with(500) |
Exactly one call, with argument 500 |
call_count |
Number of calls |
36. β¨ What is MagicMock?
MagicMock is a more feature-rich Mock that also supports Python's special (magic/dunder) methods:
from unittest.mock import MagicMock
items = MagicMock()
items.__len__.return_value = 5
print(len(items)) # 5
Other special methods: __iter__(), __getitem__(), __enter__(), __exit__(). You'll often see MagicMock with context managers, iterables, and container-like objects. For ordinary function mocking, Mock is usually enough.
βοΈ PART 9: Monkeypatch vs Mock vs Patch
π― Very important for interviews and real backend work!
| Concept | Meaning |
|---|---|
| Mocking | General technique of replacing real dependencies with test doubles |
Mock |
Creates a fake object |
MagicMock |
A Mock that also supports magic methods |
patch |
Temporarily replaces an object with a mock |
monkeypatch |
pytest fixture for temporarily changing attributes, environment, dicts, etc. |
Use monkeypatch when you want to:
- π Replace a function
- π Modify environment variables
- βοΈ Change a configuration value
- β»οΈ Have pytest restore the original value automatically
Use unittest.mock when you want to:
- π Simulate an external dependency
- π Define return values
- π₯ Simulate exceptions
- π’ Verify how many times a function was called
- π Inspect the arguments passed to a dependency
Both can overlap. You don't need to use them together in every test.
ποΈ PART 10: Putting It All Together in a Backend
37. π§ͺ How to test a real backend service
API Router
β
βΌ
User Service
β
βΌ
User Repository
β
βΌ
PostgreSQL Database
To test UserService, you don't need PostgreSQL every time. Mock the repository.
user_service.py:
class UserService:
def __init__(self, repository):
self.repository = repository
def get_user(self, user_id: int):
user = self.repository.get_by_id(user_id)
if user is None:
raise ValueError("User not found")
return user
test_user_service.py:
from unittest.mock import Mock
import pytest
from user_service import UserService
def test_get_user():
repository = Mock()
repository.get_by_id.return_value = {"id": 1, "name": "Deepika"}
service = UserService(repository)
result = service.get_user(1)
assert result["name"] == "Deepika"
repository.get_by_id.assert_called_once_with(1)
What happens:
- Create a fake repository
- Tell it what data to return
- Create the real
UserService - Call
get_user() - The service calls the mocked repository
- The mock returns the predefined user
- Assert the correct user was returned
- Verify the repository was called with ID
1
We're testing the actual service logic without a real database. That's a unit test. β
β Testing the error case
def test_get_user_not_found():
repository = Mock()
repository.get_by_id.return_value = None
service = UserService(repository)
with pytest.raises(ValueError, match="User not found"):
service.get_user(100)
pytest.raises(ValueError) means we expect the code inside to raise a ValueError. If it doesn't, the test fails. This is exception testing.
38. ποΈ What is conftest.py?
With 100+ tests, many files need the same fixtures. Instead of redefining them everywhere, pytest has a special file: conftest.py.
backend/
β
βββ app/
β βββ main.py
β βββ services/
β βββ repositories/
β
βββ tests/
β βββ conftest.py
β βββ test_users.py
β βββ test_posts.py
β βββ test_auth.py
β
βββ pyproject.toml
conftest.py:
import pytest
@pytest.fixture
def sample_user():
return {"id": 1, "name": "Deepika"}
Tests in that directory and its subdirectories can use the fixture without importing it:
def test_user_name(sample_user):
assert sample_user["name"] == "Deepika"
-
test_*.pyβ contains tests -
conftest.pyβ shared pytest configuration and fixtures -
@pytest.fixtureβ reusable test setup
39. β‘ TestClient in FastAPI
@app.get("/health")
def health_check():
return {"status": "ok"}
You could start the real server and send requests manually, but there's a simpler way: Starlette's TestClient.
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_endpoint():
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
-
TestClient(app)creates a test client for your FastAPI app -
client.get("/health")sends a simulated HTTP request through the app -
response.status_codegives the HTTP status code -
response.json()gives the response body as a Python dict
No need to start Uvicorn manually. This is API-level testing. π―
π If your app uses async resources or async test functions, you may also meet
httpx.AsyncClientandpytest-asyncio.
π§© PART 11: How Everything Fits Together
Your Python backend toolkit:
| Tool | Role |
|---|---|
| π Python | The programming language |
| π venv / Poetry | Environment and dependency management |
π pyproject.toml
|
Project metadata and tool configuration |
| π·οΈ Type hints + mypy | Type documentation and static checking |
| π§ͺ pytest | Automated testing framework |
| π Mocking & monkeypatch | Isolating dependencies during testing |
40. π Commands cheat sheet
| Command | Purpose |
|---|---|
python -m venv .venv |
Create virtual environment |
.venv\Scripts\Activate.ps1 |
Activate on Windows PowerShell |
source .venv/bin/activate |
Activate on Linux/macOS |
deactivate |
Deactivate environment |
pip install fastapi |
Install a package |
pip list |
List installed packages |
pip freeze |
Display installed versions |
pip freeze > requirements.txt |
Save dependencies |
pip install -r requirements.txt |
Install from file |
poetry init |
Initialize Poetry project |
poetry add fastapi |
Add dependency |
poetry install |
Install project dependencies |
poetry run pytest |
Run tests using Poetry |
mypy app/ |
Check application types |
pytest |
Run tests |
pytest -v |
Run tests with detailed names |
pytest -q |
Run tests in concise mode |
pytest -x |
Stop after first failure |
pytest -k login |
Run tests matching a keyword |
pytest --collect-only |
Show discovered tests |
π€ PART 12: Common Interview Questions
1. What is a virtual environment?
An isolated Python environment that keeps project-specific dependencies without affecting other projects.
2. What's the difference between venv and Poetry?
venv creates isolated environments. Poetry provides broader dependency, environment and packaging management.
3. What is pyproject.toml?
A modern Python project configuration file that stores project metadata and tool configurations.
4. What is a lock file?
A file that records exact resolved dependency versions to help reproduce a consistent environment.
5. What are type hints?
Annotations that specify expected types for variables, parameters and return values.
6. Does Python enforce type hints automatically?
No. Python generally doesn't enforce them at runtime. Static analysis tools such as mypy can check them.
7. What is mypy?
A static type checker that analyzes Python source code and reports potential type inconsistencies.
8. What is pytest?
A Python testing framework used to write and run automated tests.
9. What is a fixture?
A reusable function that provides test data or prepares resources required by tests.
10. What is parametrize?
A pytest feature that runs the same test with multiple input combinations.
11. What is monkeypatch?
A pytest fixture that temporarily modifies attributes, functions, environment variables or other values during a test.
12. What is mocking?
Replacing real dependencies with controlled fake objects to isolate the code being tested.
13. What's the difference between Mock and MagicMock?
MagicMock supports Python's magic methods in addition to everything Mock can do.
14. What's the difference between return_value and side_effect?
return_value specifies a mock's returned value. side_effect can raise exceptions or give different results on successive calls.
15. What's the difference between unit and integration testing?
Unit testing checks an isolated component. Integration testing checks how multiple components work together.
π§ͺ PART 13: Test Your Understanding!
Try answering before revealing the answers. No stress! β€οΈ
-
Two Python projects need different versions of FastAPI. What should you use?
- A) A single global installation
- B) Separate virtual environments
- C) Only type hints
-
Which file records exact resolved dependency versions in a Poetry project?
- A)
main.py - B)
conftest.py - C)
poetry.lock
- A)
-
What does
name: strmean in a function?- A)
nameis always checked at runtime - B)
nameis expected to be a string - C)
nameis converted into a string automatically
- A)
-
Which tool checks type annotations without executing your application?
- A)
mypy - B)
pytest - C) Uvicorn
- A)
-
You want to reuse a sample user object across multiple tests. What do you use?
- A)
parametrize - B) A fixture
- C)
side_effect
- A)
-
You want to run the same test with 10 different inputs. Which feature helps?
- A)
monkeypatch - B)
MagicMock - C)
parametrize
- A)
-
You want to temporarily replace an environment variable during a test. What's suitable?
- A)
monkeypatch.setenv() - B)
pytest.raises() - C)
mypy
- A)
-
You want a fake database method to return a predefined user. What do you use?
- A)
Mock.return_value - B)
venv - C)
poetry.lock
- A)
-
You want to check that a mocked email function was called exactly once with a particular address. Which method?
- A)
assert_called() - B)
assert_called_once_with() - C)
assert_not_called()
- A)
-
What does the code after
yieldin a fixture usually do?- A) Dependency installation
- B) Static checking
- C) Cleanup
π Click to reveal answers
poetry.lock records exact resolved versionsname is expected to be a string (not enforced at runtime)mypy analyzes code without running itparametrize runs one test with many inputsmonkeypatch.setenv() sets an env variable temporarilyMock.return_value defines what the fake returnsassert_called_once_with() checks both call count and arguments
ποΈ PART 14: What You Should Actually Practise
For your backend project, follow this order:
- π Create a
.venvand install FastAPI and pytest - π Create a
requirements.txt, or understand your project's existing Poetry setup - π·οΈ Add type hints to a few service and repository functions
- π§ Run mypy and understand its errors
- π§ͺ Write unit tests for your user service
- ποΈ Create shared fixtures in
conftest.py - π Use
parametrizeto test different inputs - π Use
monkeypatchto replace environment variables or functions - π Use
Mockandpatchto simulate repository and external service behavior - β‘ Write a FastAPI endpoint test using
TestClient
π‘ You don't need every tool in every project. If your repository already uses a particular dependency manager, follow its existing setup instead of introducing Poetry or another tool unnecessarily.
π Final Mental Model: Remember These Ten Things
| Topic | Remember it as |
|---|---|
π venv
|
A separate room for each project's libraries |
| π Poetry | Project dependency and packaging manager |
π pyproject.toml
|
Project configuration file |
| π·οΈ Type hints | Expected types written in code |
| π§ mypy | Checks type consistency without running the app |
| π§ͺ pytest | Automatically tests your code |
| π§° Fixtures | Prepare reusable test resources |
| π Parametrize | Same test, different inputs |
| π Monkeypatch | Temporarily change something |
| π Mocking | Replace real dependencies with fake ones |
π What's Next?
Don't just memorize definitions. Write one small test for a real function from your own backend. That's when everything clicks! π‘
If this post helped you, drop a β€οΈ or π¦ and share it with a friend learning Python. Happy coding! π©βπ»β¨
Top comments (0)