📌 Who This Article Is For
- QA engineers and developers who want to understand what API testing is
- Those who want to understand the difference between UI testing and API testing
- Engineers who want to start automated testing with Python
- Those who want to learn the basics of pytest and requests from scratch
✅ What You Will Learn
- The difference between API testing and UI testing, and when to use each
- How to send HTTP requests using the requests library
- The basic pattern for running API tests automatically with pytest
- How to auto-generate HTML reports and use them in your portfolio
Do you think API testing sounds difficult? In fact, by combining just two libraries — requests and pytest — you can write production-level API tests.
This article walks you through everything from the basic concepts of API testing to environment setup and actual code. Even if you only have experience with UI testing, by the end of this article you will have the confidence to say “I can write API tests too.”
00. What Is API Testing?
API testing is a way to verify that the underlying logic (API) of an application is working correctly.
While UI testing operates a browser to check the screen, API testing sends HTTP requests directly and verifies the responses — no browser required.
| UI Testing | API Testing | |
|---|---|---|
| What it checks | Screen, appearance, user interaction | Data, logic, security |
| Browser | Required | Not required |
| Speed | Slow 🐢 | Fast ⚡ |
| Fragility | Breaks when UI changes | Stable as long as logic doesn’t change |
💡 Key Takeaway:UI testing and API testing are not about which is better — they serve different purposes. Being able to do both significantly increases your value as a QA engineer.
01. Why Is API Testing Important?
Nearly all modern web apps use a structure where the frontend (UI) and backend (API) are separated.
If the API breaks, even if the screen looks perfectly fine, data won’t be saved, login will fail, and payments will not go through — all critical problems.
API tests also run 10 to 100 times faster than UI tests, making them ideal for integration into CI/CD pipelines to run automatically before every deployment.
# What API testing looks like
# No browser! Communicate with the server using code alone.
import requests
response = requests.get("https://jsonplaceholder.typicode.com/users/1")
print(response.status_code) # 200
print(response.json()) # {"id": 1, "name": "Leanne Graham", ...}
💡 Key Takeaway:Just 3 lines of code to send a request to an API and check the response. This is the foundation of API testing.
02. Environment Setup
Installing the Required Libraries
Just install these three libraries and you are ready to go.
pip install requests pytest pytest-html
| Library | Role |
|---|---|
requests |
Send HTTP requests to the API |
pytest |
Run and manage tests |
pytest-html |
Auto-generate HTML reports |
pytest.ini Configuration
Place pytest.ini in your project root so that an HTML report is automatically generated every time you run tests.
# pytest.ini
[pytest]
addopts = --html=report.html --self-contained-html
💡 Tip:Once configured, running pytest test_api.py -v alone will automatically generate an HTML report every time. Great for portfolio evidence too.
03. Basics of the requests Library
Using HTTP Methods
Methods are available for each HTTP verb.
import requests
# GET: Retrieve data
response = requests.get("https://jsonplaceholder.typicode.com/users/1")
# POST: Create data
response = requests.post(
"https://jsonplaceholder.typicode.com/users",
json={"name": "Taro", "email": "taro@example.com"}
)
# PUT: Update data
response = requests.put(
"https://jsonplaceholder.typicode.com/users/1",
json={"name": "Updated Taro"}
)
# DELETE: Delete data
response = requests.delete("https://jsonplaceholder.typicode.com/users/1")
Checking the Response
response = requests.get("https://jsonplaceholder.typicode.com/users/1")
print(response.status_code) # Status code: 200
print(response.json()) # Response body (JSON format)
print(response.headers) # Response headers
💡 Key Takeaway:Using response.json() returns the response JSON as a Python dictionary, which you can use directly in assertions.
04. Basics of pytest
Writing Tests
Simply start a function name with test_ and pytest will automatically recognize it as a test.
import requests
def test_get_user():
"""Verify that user information can be retrieved correctly"""
response = requests.get("https://jsonplaceholder.typicode.com/users/1")
assert response.status_code == 200, f"Expected: 200, Got: {response.status_code}"
assert response.json()["id"] == 1
assert "name" in response.json()
How to Run Tests
# Run all tests
pytest
# Run with verbose output
pytest -v
# Run a specific file only
pytest test_api.py -v
Example Output
test_api.py::test_get_user PASSED [ 33%]
test_api.py::test_create_user PASSED [ 66%]
test_api.py::test_delete_user PASSED [100%]
3 passed in 1.23s ✅
💡 Key Takeaway:When an assert fails, pytest automatically shows what the expected value was and what was actually returned, making debugging much easier.
05. Combining pytest + requests
Combining these two lets you write production-level API tests. Let’s try it out using JSONPlaceholder, a free mock API.
import requests
import pytest
BASE_URL = "https://jsonplaceholder.typicode.com"
def test_get_all_users():
"""Verify that all users can be retrieved"""
response = requests.get(f"{BASE_URL}/users")
assert response.status_code == 200
assert len(response.json()) == 10 # Should return 10 users
def test_create_user():
"""Verify that a user can be created"""
new_user = {"name": "Yoshitsugu", "email": "test@example.com"}
response = requests.post(f"{BASE_URL}/users", json=new_user)
assert response.status_code == 201
assert response.json()["name"] == "Yoshitsugu"
def test_delete_user():
"""Verify that a user can be deleted"""
response = requests.delete(f"{BASE_URL}/users/1")
assert response.status_code == 200
⚠️ Note:JSONPlaceholder is a mock API, so data is not actually saved or deleted. It is safe to use for learning purposes.
06. Pitfalls & Lessons Learned
Here are the key issues I encountered during implementation. I hope this helps others who run into the same problems.
① Confusing requests with pytest-requests
When I first searched for “a library to send HTTP requests with pytest,” I found something called pytest-requests and installed it by mistake. In reality, you use two separate libraries — requests (HTTP client) and pytest (testing framework) — in combination.
# ❌ pytest-requests is not what we need here
pip install pytest-requests
# ✅ Correct installation
pip install requests pytest pytest-html
💡 Key Takeaway:requests is for HTTP requests, pytest is for running tests. They serve different roles, so install and use both.
② Getting a KeyError from response.json()
When trying to extract a value from the response JSON, I specified a key that didn’t exist and got a KeyError. It’s important to check the response content before accessing keys.
# ❌ KeyError if the key doesn't exist
user_id = response.json()["userId"] # KeyError!
# ✅ First check the content with print()
print(response.json())
# {'id': 1, 'name': 'Leanne Graham', ...}
# ✅ Use get() to avoid KeyError
user_id = response.json().get("id") # Returns None instead of raising an error
💡 Key Takeaway:For any unfamiliar API, always use print(response.json()) to check the response structure before writing assertions.
③ Hard to Tell Why an assert Failed
At first I only wrote assert response.status_code == 200, so when it failed I had no idea what was actually returned, making debugging slow.
# ❌ Little information when it fails
assert response.status_code == 200
# ✅ Add a message to understand the cause immediately
assert response.status_code == 200, f"Expected: 200, Got: {response.status_code}"
💡 Key Takeaway:Using the assert condition, "message" format lets you see what was expected vs. what was actually returned the moment a test fails.
④ Didn’t Know pytest’s Naming Rules
pytest failed to detect my tests automatically. The cause was that my file names and function names didn’t follow pytest’s naming conventions.
# ❌ pytest won't detect these
# Filename: api_test.py (doesn't start with test_)
def check_user(): # doesn't start with test_
assert True
# ✅ pytest detects these automatically
# Filename: test_api.py (starts with test_)
def test_check_user(): # starts with test_
assert True
⚠️ Note:File names must start with test_ or end with _test, and function names must start with test_. These are the two basic rules of pytest.
⑤ Double Slash in URL Due to Trailing Slash in BASE_URL
When I added a trailing / to BASE_URL and then concatenated a path, the URL became //users with a double slash, causing requests to fail.
# ❌ Results in a double slash
BASE_URL = "https://jsonplaceholder.typicode.com/"
response = requests.get(f"{BASE_URL}/users")
# → https://jsonplaceholder.typicode.com//users (WRONG)
# ✅ Remove the trailing slash from BASE_URL
BASE_URL = "https://jsonplaceholder.typicode.com"
response = requests.get(f"{BASE_URL}/users")
# → https://jsonplaceholder.typicode.com/users (CORRECT)
💡 Key Takeaway:Avoid trailing slashes in BASE_URL and consistently add a leading / to each path to prevent confusion.
07. Frequently Asked Questions (FAQ)
Q. What are the benefits of using pytest for API testing?
A. The biggest advantage is that you can write tests intuitively in Python and integrate them easily with CI/CD pipelines. You can also use pytest-html to auto-generate HTML reports, which significantly reduces the effort of creating test evidence.
Q. Which is better — requests or httpx?
A. It depends on your use case. Use requests for simple synchronous processing, and httpx if you need asynchronous processing (async/await). For API testing beginners, requests is simpler and recommended.
Q. Should I start with UI testing or API testing?
A. Starting with API testing is recommended. It runs faster and the environment setup is simpler, making it ideal for learning the basics of test automation. Even if you are already familiar with UI testing, adding API tests helps you stand out as an engineer who can handle both.
Q. What if I don’t have an API to test against?
A. Use publicly available practice APIs. JSONPlaceholder (user/post CRUD) and Restful Booker (authentication flow) are ideal for portfolio API testing. Both are free to use.
Q. What is a pytest fixture?
A. A fixture is a mechanism for sharing setup and teardown logic across tests. For example, if you write token acquisition in a fixture, you can reuse it across multiple test cases. It is an important feature for reducing code duplication and keeping your test structure maintainable.
08. Summary
This article covered the basic concepts of API testing and how to use pytest and requests.
| Key Point | Details |
|---|---|
| What is API testing | Testing that sends HTTP requests without a browser and verifies responses |
| Role of requests | Sending HTTP requests to the API and receiving responses |
| Role of pytest | Running and managing tests, generating HTML reports |
| Strength of the combination | Achieve production-level API testing with simple, readable code |
Even if you only have UI testing experience, learning just requests and pytest is enough to start writing API tests. Publishing your code on GitHub lets you say “I can do both UI and API testing!” — a powerful way to stand out on Upwork and LinkedIn.
