QA Test an E-Commerce API

Build a Postman and Python test suite for a live ecommerce API.

Introduction

30 Second Summary

Every time you add something to an online cart and the price shows up wrong, or a product page loads blank, a QA engineer somewhere missed a test. Finding those bugs before users do is what separates a polished product from a broken one.

In this project, you will build a complete QA test suite for a live ecommerce API. You'll start by exploring endpoints in Postman, then automate your checks with Python and pytest so they run in seconds.

What You'll Build

Picture running a single command and watching a dozen tests fly through products, categories, and error cases, confirming the API behaves exactly as promised.

By the end of this project, you'll have:

  • A Postman collection with scripted assertions that validate live API responses as you send requests.
  • An automated Python test suite that checks happy paths, error handling, and multiple data scenarios with one command.
  • JSON schema validation tests that catch breaking contract changes the moment they happen.
  • Secret Mission: Test the full authentication flow by logging in, extracting a JWT token, and verifying its structure.

Are there any prerequisites?

You should have basic Python experience (functions, imports, loops) and familiarity with virtual environments and pytest. Postman should already be installed on your machine.

Before We Start

Every test suite needs a stable foundation. If your test dependencies bleed into other Python projects on your machine, reproducing failures and sharing your work with teammates becomes unreliable.

In this step, you'll create an isolated project environment, install the testing libraries, and set up the file structure your test suite lives in.

In this step, get ready to:
  • Create a project folder and virtual environment.
  • Install the testing packages.
  • Set up the file structure and verify everything works.
Create your project folder and virtual environment

A virtual environment is a self-contained copy of Python and its packages that lives inside your project folder. This means the libraries you install for this test suite won't interfere with anything else on your system.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Cursor and press Enter to open it.
  • Open the integrated terminal by pressing Ctrl+` (backtick) on both macOS and Windows.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir ecommerce-qa
cd ecommerce-qa
  • Now open this folder as your workspace in Cursor.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop and select the ecommerce-qa folder, then click Open.
  • Open a fresh terminal in the workspace by pressing Ctrl+`.

Your terminal prompt now shows you're inside the ecommerce-qa folder.

  • Create a virtual environment by running this command:
python -m venv venv
  • Activate the virtual environment:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate

You should see (venv) appear at the beginning of your terminal prompt. This confirms the virtual environment is active.

Don't see (venv) in your prompt?

On Windows, if you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell first, then try the activation command again.

On macOS, make sure you used source before the path. Running the path alone without source will not activate the environment.

Help me activate my virtual environment.

Install testing packages

Your test suite depends on three libraries: a test runner, an HTTP client to send requests, and a schema validator to check response structures.

  • Install all three packages by running this command:
pip install pytest==9.1.1 requests==2.34.2 jsonschema==4.26.0

What do these packages do?

  • pytest is your test runner. It discovers test files, runs them, and reports pass/fail results.
  • requests is an HTTP client that lets your Python code send GET, POST, and other requests to APIs.
  • jsonschema validates that JSON responses match a defined structure. If a field is missing or has the wrong type, it raises an error.

You should see output showing each package downloading and installing successfully, ending with a line like Successfully installed followed by the package names.

Seeing installation errors?

Make sure (venv) appears in your terminal prompt. If it doesn't, your virtual environment isn't active and packages may install to the wrong location.

If you see a permissions error, do not use sudo. Instead, deactivate and reactivate your virtual environment.

Help me fix my pip install error.

Create your project structure and verify

pytest discovers tests by looking for files that start with test_ inside a tests folder. The __init__.py file marks the folder as a Python package so imports work correctly.

  • Create the tests folder and the empty files inside it by running these commands:

macOS

mkdir tests
touch tests/__init__.py tests/conftest.py tests/test_products.py

Windows

mkdir tests
New-Item tests\__init__.py -ItemType File
New-Item tests\conftest.py -ItemType File
New-Item tests\test_products.py -ItemType File
  • Confirm the files exist by running:
ls tests

You should see __init__.py, conftest.py, and test_products.py listed in the output.

What is conftest.py?

conftest.py is a special pytest file where you define fixtures. Fixtures are reusable setup functions that provide shared data or resources to your tests. You'll add fixtures here in the next step.

Before you run this next command, what version number do you expect to see?

  • Verify pytest is installed correctly by running:
pytest --version

You should see pytest 9.1.1 printed in the output. This confirms your testing environment is ready.

Command not found?

If you see command not found, your virtual environment is likely not active. Check for (venv) in your prompt and reactivate if needed.

Help me get pytest working.

Your environment is ready. Next up, you'll explore the API in Postman to understand what you're testing before writing any automated code.

Explore the API with Postman

Your project environment is set up and your test files are ready. Now it's time to meet the API you'll be testing throughout this project.

Before writing automated tests, you need to understand what an API actually returns. Postman lets you send requests interactively and write quick assertions on the fly. This is how QA engineers do exploratory testing in the real world.

In this step, get ready to:
  • Send requests to a live ecommerce API and observe its responses.
  • Write test scripts that automatically validate response data.
  • Run all requests together using the Collection Runner.
Create a collection and send your first requests

A Postman collection groups related API requests together. You will create one to hold all the requests for FakeStoreAPI, the free ecommerce API you are testing.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Postman and press Enter to open it.
  • In the left sidebar, click New (the + icon at the top).
  • Select Collection.
  • Name it FakeStore QA and press Enter.

Your collection now appears in the sidebar. Time to add your first request.

  • Click Add a request inside your new collection (or right-click the collection and select Add request).
  • Name the request Get All Products.
  • Make sure the method is set to GET.
  • Paste this URL into the request URL field:
https://fakestoreapi.com/products
  • Click Send.

You should see a JSON array of 20 products in the response body. Each product has id, title, price, description, category, and image fields.

What is FakeStoreAPI?

FakeStoreAPI is a free, public REST API that simulates an ecommerce store. It returns realistic product, cart, and user data without requiring any registration or API key.

This makes it perfect for practicing QA testing. The responses are predictable enough to write assertions against, but realistic enough to mirror production APIs.

  • Add a second request to your collection. Name it Get Single Product.
  • Set the method to GET and paste this URL:
https://fakestoreapi.com/products/1
  • Click Send.

You should see a single product object with id equal to 1 and its full details.

  • Add a third request named Get Categories.
  • Set the method to GET and paste this URL:
https://fakestoreapi.com/products/categories
  • Click Send.

You should see an array of category strings. This confirms the API is responding and you know the shape of its data.

Not seeing any response data?

  • Check that the URL starts with https:// (not http://).
  • Make sure you are connected to the internet. FakeStoreAPI is a live external service.
  • Confirm the method dropdown shows GET, not POST or another method.

Help me troubleshoot why my Postman request is not returning data from FakeStoreAPI.

Write Postman test scripts

Sending requests tells you what the API returns right now. Test scripts let Postman automatically check the response every time you send, so you catch problems without reading the JSON manually.

  • Click on your Get All Products request in the sidebar.
  • Click the Scripts tab in the request panel.
  • Select Post-response to open the post-response script editor.
  • Paste the following test script into the editor:
pm.test("Status is 200", function() {
  pm.response.to.have.status(200);
});
pm.test("Response is an array", function() {
  const data = pm.response.json();
  pm.expect(data).to.be.an("array");
  pm.expect(data.length).to.be.greaterThan(0);
});

What does this script do?

  • pm.test() defines a named test case. If the assertion inside fails, Postman marks that test as FAIL.
  • pm.response.to.have.status(200) checks that the HTTP status code is 200 (success).
  • pm.response.json() parses the response body as JSON so you can inspect its structure.
  • pm.expect(data).to.be.an("array") uses the built-in Chai assertion library to verify the response is an array, not just a single object.
  • Click Send to run the request with your new script.
  • In the response section at the bottom, click the Test Results tab.

You should see two green PASS results: "Status is 200" and "Response is an array".

  • Now click on your Get Single Product request.
  • Click the Scripts tab, then select Post-response.
  • Paste this test script to verify the product has the expected fields:
pm.test("Product has required fields", function() {
  const product = pm.response.json();
  pm.expect(product).to.have.property("id");
  pm.expect(product).to.have.property("title");
  pm.expect(product).to.have.property("price");
});

What does this script do?

pm.expect(product).to.have.property("id") checks that the response object contains an id field. The same check runs for title and price. If the API ever removes or renames one of these fields, this test catches it instantly.

  • Click Send.
  • Click the Test Results tab in the response section.

You should see a green PASS for "Product has required fields".

Seeing a FAIL result?

  • Make sure you pasted the script into the Post-response section, not the Pre-request section. Pre-request scripts run before the response arrives, so pm.response would be empty.
  • Check that the request URL is exactly https://fakestoreapi.com/products/1 with no trailing spaces.

Why is my Postman test script returning FAIL when I send the request?

Run the collection

You have tested individual requests one at a time. The Collection Runner executes all requests in sequence and reports every test result in one view. This is how you validate an entire API surface quickly.

Before you run it, predict: do you think all tests will pass, or will the categories request (which has no test script) cause any failures?

  • In the left sidebar, click Collections and select your FakeStore QA collection.
  • Click Run (the play icon at the top of the collection view).
  • On the Functional tab, select Run manually.
  • Click Start run.

All three requests execute in sequence. You should see green results for the two requests that have test scripts. The categories request runs successfully (status 200) but has no assertions to evaluate.

Why does the categories request show no test results?

A request without a post-response script simply returns its response. Postman does not fail it or pass it. It only reports on tests you explicitly write. This is why writing assertions matters. A 200 status code alone does not guarantee the data is correct.

Collection Runner not showing your requests?

  • Make sure all three requests are saved inside the FakeStore QA collection, not floating as unsaved tabs.
  • Press Cmd+S (macOS) or Ctrl+S (Windows) on each request tab to save before running.

Help me figure out why my Postman Collection Runner is not showing all my requests.

You now have a working Postman collection with scripted assertions that validate the API on every send. Next up, you will translate these same checks into automated Python tests that can run without a GUI.

Write Your First Automated Python Tests

Your Postman collection proves the API works when you poke at it manually. But manual testing does not scale.

Automated tests need to run without a GUI, integrate into CI pipelines, and repeat hundreds of checks in seconds. pytest gives you exactly that. In this step, you will write Python test functions that validate the same endpoints you explored in Postman.

In this step, get ready to:
  • Create a reusable fixture that provides the API base URL to every test.
  • Write three test functions covering products, single product lookup, and categories.
  • Run the test suite and see all three tests pass.
Set up the base URL fixture

Every test function in this project needs the FakeStoreAPI base URL. Instead of hardcoding it in each file, you will define a pytest fixture that returns the URL once and shares it across the entire test session.

  • In Cursor, open tests/conftest.py and add the following code:
import pytest
import requests


@pytest.fixture(scope="session")
def base_url():
    return "https://fakestoreapi.com"

What does this code do?

  • import pytest gives you access to the @pytest.fixture decorator.
  • scope="session" means the fixture runs once for the entire test run, not once per test function. This keeps things efficient.
  • Any test that includes base_url in its function signature automatically receives the returned string.
  • Save conftest.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved conftest.py and move on.

ⓧ I'd like to double check the full code

import pytest
import requests


@pytest.fixture(scope="session")
def base_url():
    return "https://fakestoreapi.com"
Write the first test function

Your first test mirrors what you already proved in Postman: fetching all products returns a 200 status and a non-empty array with the expected fields.

  • Open tests/test_products.py and add the following code:
import pytest


def test_get_all_products(base_url):
    import requests
    response = requests.get(f"{base_url}/products")
    assert response.status_code == 200

    products = response.json()
    assert isinstance(products, list)
    assert len(products) > 0

    first_product = products[0]
    expected_fields = ["id", "title", "price", "description", "category", "image"]
    for field in expected_fields:
        assert field in first_product, f"Missing field: {field}"

What does this code do?

  • base_url in the function signature tells pytest to inject the fixture you defined in conftest.py.
  • assert response.status_code == 200 confirms the request succeeded. If the API returns anything else, pytest marks the test as FAILED.
  • The loop checks every expected field exists in the first product. The custom message f"Missing field: {field}" tells you exactly which field is absent if the assertion fails.
  • Save test_products.py.
  • Run the test to confirm your fixture wiring works by running this command:
pytest tests/test_products.py -v

You should see output showing test_get_all_products PASSED with a summary of 1 passed.

Seeing a FAILED or error?

  • If you see ModuleNotFoundError: No module named 'requests', make sure your virtual environment is activated. You should see (venv) at the start of your terminal prompt.
  • If you see fixture 'base_url' not found, confirm that conftest.py is saved inside the tests folder (not the project root).

Still stuck? Help me debug why my pytest fixture is not being found.

Add tests for single products and categories

Two more tests will round out your happy-path coverage: fetching a single product by ID, and fetching the list of categories.

  • In tests/test_products.py, add this function below your existing test:
def test_get_single_product(base_url):
    import requests
    response = requests.get(f"{base_url}/products/1")
    assert response.status_code == 200

    product = response.json()
    assert product["id"] == 1

What does this test verify?

This sends GET /products/1 and checks that the returned product has id equal to 1. If the API ever returned the wrong product for a given ID, this test would catch it.

  • Add one more function below test_get_single_product:
def test_get_categories(base_url):
    import requests
    response = requests.get(f"{base_url}/products/categories")
    assert response.status_code == 200

    categories = response.json()
    assert isinstance(categories, list)
    assert len(categories) > 0
    for category in categories:
        assert isinstance(category, str)

What does this test verify?

This fetches the categories list and checks that every entry is a string. If the API schema changed and started returning objects instead of strings, this test would fail immediately.

  • Save test_products.py.

✔️ Awesome, I've got everything!

Great. Double check you have saved test_products.py and move on to run the full suite.

ⓧ I'd like to double check the full code

Your test_products.py should have these sections in order:

  • The import pytest line at the very top.
  • test_get_all_products function (checks status 200, response is a non-empty list, first product has all expected fields).
  • test_get_single_product function (checks status 200 and product ID is 1).
  • test_get_categories function (checks status 200, response is a non-empty list of strings).

Scroll up and confirm each chunk is present and in order.

Run the complete test suite

You have three test functions covering the same endpoints you validated in Postman. Time to run them all together.

Before you run the command below, predict: how many tests do you expect to see, and will they all pass?

  • Run the full test file in verbose mode by running this command:
pytest tests/test_products.py -v

You should see each test listed with PASSED next to it, and a final summary of 3 passed.

What should I see?

The verbose output lists each test with its full path and status:

tests/test_products.py::test_get_all_products PASSED      [ 33%]
tests/test_products.py::test_get_single_product PASSED    [ 66%]
tests/test_products.py::test_get_categories PASSED        [100%]

======================== 3 passed in X.XXs ========================

The percentage shows progress through the collected tests. The final line confirms all 3 passed.

One or more tests FAILED?

  • Check that each test function accepts base_url as a parameter. A missing parameter means pytest cannot inject the fixture.
  • If you see a ConnectionError, confirm you have internet access. The tests call a live API at https://fakestoreapi.com.
  • If test_get_all_products fails on the field check, make sure the expected_fields list matches exactly: id, title, price, description, category, image.

Need more help? Help me figure out why my pytest tests are failing.

Why Python instead of staying in Postman?

Postman is perfect for exploratory testing. You can poke at endpoints, inspect responses, and iterate quickly. But when you need to run 50 checks on every code push without opening a GUI, Python with pytest is the standard.

Your test file can run in a CI/CD pipeline, generate reports, and scale to hundreds of assertions. Postman gave you the understanding. Python gives you the automation.

Three tests, three green passes. You now have automated checks that can run anytime without opening Postman. Next up, you will use parametrize to multiply those tests across many inputs without writing repetitive code.

Data-Driven Testing with Parametrize

Your test suite validates the FakeStoreAPI with three focused checks. That works for a handful of cases, but a real ecommerce API has twenty products, four categories, and dozens of edge cases to cover.

Writing a separate function for every single scenario creates unmaintainable bloat. In this step, you'll use pytest's parametrize decorator to run the same test logic across many inputs automatically.

In this step, get ready to:
  • Parametrize product ID tests across multiple products.
  • Parametrize category tests across all four categories.
  • Add a reusable session fixture for efficient HTTP connections.
Parametrize product ID tests

The @pytest.mark.parametrize decorator takes a parameter name and a list of values. Pytest automatically generates one separate test case for each value, running the same function body with each input.

  • In tests/test_products.py, add this parametrized test function at the end of the file:
@pytest.mark.parametrize("product_id", [1, 2, 5, 10, 20])
def test_get_product_by_id(base_url, product_id):
    import requests
    response = requests.get(f"{base_url}/products/{product_id}")
    assert response.status_code == 200

    product = response.json()
    assert product["id"] == product_id
    assert "title" in product
    assert "price" in product

What does this code do?

  • @pytest.mark.parametrize("product_id", [1, 2, 5, 10, 20]) tells pytest to run this function five times, once for each ID in the list.
  • Each run injects a different product_id value into the function parameter, so you get five separate test results from one function.
  • The assertions verify that each returned product's id matches the requested ID and that required fields exist.
  • Save test_products.py.
  • Run the tests by running this command:
pytest tests/test_products.py -v

You should see 8 tests passing. The original 3 plus 5 new parametrized cases, each named with the product ID in brackets (like test_get_product_by_id[1], test_get_product_by_id[2]).

Only seeing 3 tests?

  • Make sure you added the new function AFTER your existing tests, not inside one of them. Check your indentation is at the top level (no extra indent).
  • Confirm the function name starts with test_. Pytest only discovers functions with that prefix.

Help me debug why pytest isn't finding my parametrized test function.

Parametrize category tests

The FakeStoreAPI has four product categories. A parametrized test can check that filtering by each category actually returns products belonging to that category.

  • In tests/test_products.py, add this parametrized category test below the product ID test you just wrote:
@pytest.mark.parametrize("category", [
    "electronics",
    "jewelery",
    "men's clothing",
    "women's clothing",
])
def test_get_products_by_category(base_url, category):
    import requests
    response = requests.get(f"{base_url}/products/category/{category}")
    assert response.status_code == 200

    products = response.json()
    assert isinstance(products, list)
    assert len(products) > 0
    for product in products:
        assert product["category"] == category

What does this code do?

  • The decorator lists all four categories from the API. Pytest generates one test case per category.
  • Each case fetches products filtered by that category, then checks that every returned product actually belongs to the requested category.
  • This catches a real bug: if the API returned electronics when you asked for jewelery, the assertion on product["category"] == category would fail.
  • Save test_products.py.
  • Run the tests again by running:
pytest tests/test_products.py -v

You should now see 12 tests passing. The 3 originals, 5 product ID cases, and 4 category cases, all generated from just 5 test functions.

Seeing a failure on men's or women's clothing?

  • Check that the category strings in your parametrize list match the API exactly. The API uses men's clothing with an apostrophe, not mens clothing.
  • Similarly, the API spells it jewelery (not "jewellery" or "jewelry"). Match the API's exact spelling.

Help me fix my parametrized category test failures.

Add a reusable session fixture

Every test function currently creates its own HTTP connection with import requests and requests.get(). In production test suites, you reuse a single requests.Session object that pools connections and runs faster.

  • Open tests/conftest.py and replace its contents with the following:
import pytest
import requests


@pytest.fixture(scope="session")
def base_url():
    return "https://fakestoreapi.com"


@pytest.fixture(scope="session")
def session():
    with requests.Session() as s:
        yield s

What does the session fixture do?

  • requests.Session() creates a persistent HTTP connection pool. Instead of opening and closing a TCP connection for every request, the session reuses existing connections.
  • scope="session" means one Session object is shared across all tests in the entire test run. It's created once, yielded to every test that requests it, then cleaned up at the end.
  • The with block ensures the session is properly closed after all tests finish, even if a test raises an exception.
  • Save conftest.py.

Now update every test function in test_products.py to use this shared session instead of importing requests inside each function. The pattern is the same for all five functions.

  • In tests/test_products.py, find the first test function. It currently looks like this:
def test_get_all_products(base_url):
    import requests
    response = requests.get(f"{base_url}/products")
  • Apply three changes to every function: add session as the first parameter, remove the import requests line inside the function, and change requests.get(...) to session.get(...). After the change, that function looks like this:
def test_get_all_products(session, base_url):
    response = session.get(f"{base_url}/products")
  • Apply the same pattern to all five functions: test_get_all_products, test_get_single_product, test_get_categories, test_get_product_by_id, and test_get_products_by_category.
  • While you're updating test_get_single_product, also add type assertions for the title and price fields so it looks like this:
def test_get_single_product(session, base_url):
    response = session.get(f"{base_url}/products/1")
    assert response.status_code == 200

    product = response.json()
    assert product["id"] == 1
    assert isinstance(product["title"], str)
    assert isinstance(product["price"], (int, float))

Why add type assertions?

Checking that title is a string and price is a number catches a class of bug that checking for field existence alone misses. If the API returned "price": "19.99" (a string instead of a number), your original test would pass but downstream code parsing the price would break.

  • Also remove the standalone import requests line at the top of test_products.py if you had one. The only import this file needs is import pytest (for the parametrize decorator).
  • Save test_products.py.

Before you run the suite again, what do you expect to see? The same 12 tests, or something different?

  • Run the full test suite by running:
pytest tests/test_products.py -v

You should see the same 12 tests all passing. The refactor changed how connections are managed, not what the tests check.

Seeing fixture 'session' not found?

  • Make sure you saved conftest.py after adding the session fixture. Pytest loads fixtures from conftest.py automatically, but only if the file is saved.
  • Check that your conftest.py is inside the tests/ folder (not in the project root).
  • Verify the fixture function is named exactly session (matching the parameter name in your test functions).

Help me fix the fixture not found error in my pytest tests.

✔️ Awesome, I've got everything!

Great. Double check you've saved both conftest.py and test_products.py.

ⓧ I'd like to double check the full code

Here's what tests/conftest.py should look like:

import pytest
import requests


@pytest.fixture(scope="session")
def base_url():
    return "https://fakestoreapi.com"


@pytest.fixture(scope="session")
def session():
    with requests.Session() as s:
        yield s

For tests/test_products.py, confirm these five functions are present in order. Each should use session as the first parameter and call session.get() instead of requests.get():

  • test_get_all_products(session, base_url) checks status 200, response is a list, and first product has all expected fields.
  • test_get_single_product(session, base_url) checks status 200, id equals 1, title is a string, and price is a number.
  • test_get_categories(session, base_url) checks status 200, response is a non-empty list of strings.
  • test_get_product_by_id(session, base_url, product_id) parametrized over IDs 1, 2, 5, 10, 20.
  • test_get_products_by_category(session, base_url, category) parametrized over electronics, jewelery, men's clothing, women's clothing.

The only import at the top of the file should be import pytest. No import requests anywhere in this file.

You've gone from 3 tests to 12 without writing 12 separate functions. Parametrize scales your coverage without scaling your code. Next up, you'll write tests for what happens when things go wrong.

Negative Testing and Schema Validation

Your test suite has 12 passing tests. They all validate happy paths where the API behaves perfectly.

If the API started returning products without a price field, or returned a string where a number should be, your current tests would not catch it. Negative testing verifies error handling. Schema validation catches contract violations automatically.

In this step, get ready to:
  • Write negative tests that verify how the API handles bad input and nonexistent resources.
  • Add JSON schema validation that catches response structure changes automatically.
  • Run the full test suite across both test files in a single command.
Write negative tests

Negative tests intentionally send bad data or request nonexistent resources to verify the API handles them gracefully.

  • In your tests folder, create a new file called test_negative.py.
  • Add the import and first test function by pasting this code:
import pytest


def test_get_nonexistent_product(session, base_url):
    response = session.get(f"{base_url}/products/99999")
    assert response.status_code == 200

    product = response.json()
    assert product is None, "Expected null for nonexistent product ID"

What does this test do?

This test requests a product ID that does not exist (99999). Most APIs would return a 404 status code for a missing resource.

FakeStoreAPI returns status 200 with a null body instead. That is a real finding you would report as a QA engineer. The test documents this behavior by asserting the response is None.

  • Save test_negative.py.
  • Run the test to confirm it passes:
pytest tests/test_negative.py -v

You should see 1 passed test in the output.

Test failing with AssertionError?

  • Make sure you are asserting product is None (not product == None). Python uses is for None comparisons.
  • Check that the URL ends with /products/99999 and not a different path.

Why is my negative test failing?

Now add two more negative tests that cover different edge cases.

  • Below test_get_nonexistent_product, add these two test functions:
def test_create_product_missing_fields(session, base_url):
    response = session.post(f"{base_url}/products", json={})
    data = response.json()
    assert "id" in data


def test_invalid_category(session, base_url):
    response = session.get(f"{base_url}/products/category/nonexistent")
    assert response.status_code == 200

    products = response.json()
    assert isinstance(products, list)
    assert len(products) == 0

What do these tests verify?

  • test_create_product_missing_fields sends a POST with an empty body. FakeStoreAPI simulates creation and still returns an id field. This documents the API's permissive validation behavior.
  • test_invalid_category requests a category that does not exist. Instead of an error, the API returns an empty array. Your test asserts the array has zero items.
  • Save test_negative.py and run the tests again:
pytest tests/test_negative.py -v

You should see 3 passed tests. All three negative tests confirm that the API does not crash on bad input.

Seeing a connection error on the POST test?

  • Make sure you are using session.post (not session.get). The POST method is required for creating a product.
  • Check that json={} is passed as a keyword argument, not a positional argument.

Help me fix my negative test failures.

Add JSON schema validation

Your negative tests check behavior. Schema validation checks the shape of every response against a contract. If a field disappears or changes type, the schema test catches it immediately.

  • At the top of test_negative.py, find this line:
import pytest
  • Add this import directly below it:
from jsonschema import validate, ValidationError
  • Below both imports (and above your test functions), add this schema definition:
PRODUCT_SCHEMA = {
    "type": "object",
    "required": ["id", "title", "price", "description", "category", "image"],
    "properties": {
        "id": {"type": "integer"},
        "title": {"type": "string"},
        "price": {"type": "number"},
        "description": {"type": "string"},
        "category": {"type": "string"},
        "image": {"type": "string"},
    },
}

What is a JSON schema?

A JSON schema is a contract that defines what a valid response looks like. It specifies which fields are required and what type each field must be.

The validate() function checks an instance against the schema. If any field is missing or has the wrong type, it raises a ValidationError immediately. This turns a silent contract violation into a loud test failure.

  • Below test_invalid_category, add this schema validation test:
def test_product_matches_schema(session, base_url):
    response = session.get(f"{base_url}/products/1")
    assert response.status_code == 200

    product = response.json()
    validate(instance=product, schema=PRODUCT_SCHEMA)

What does this test do?

This test fetches a single product and passes it to validate(). If the product has all required fields with the correct types, the function returns silently (test passes). If any field is missing or has the wrong type, it raises ValidationError and the test fails.

  • Save test_negative.py and run to confirm the schema test passes:
pytest tests/test_negative.py -v

You should see 4 passed tests. The schema validation test confirms product 1 matches your contract.

  • Now add a test that validates every product in the catalog against the same schema. Add this below test_product_matches_schema:
def test_all_products_match_schema(session, base_url):
    response = session.get(f"{base_url}/products")
    assert response.status_code == 200

    products = response.json()
    for product in products:
        validate(instance=product, schema=PRODUCT_SCHEMA)

Why validate all products?

Testing one product proves the schema works. Testing all 20 products proves the API is consistent. If even one product has a missing field or wrong type, this test catches it. In production QA, this kind of sweep test finds edge cases that single-item tests miss.

  • Save test_negative.py and run again:
pytest tests/test_negative.py -v

You should see 5 passed tests in test_negative.py.

Seeing a ValidationError in the output?

  • A ValidationError means a product does not match your schema. Check that your PRODUCT_SCHEMA uses "number" for the price type (not "integer"). Prices can be floats like 9.99.
  • Make sure the schema is defined above your test functions, not inside one of them.

Help me fix my schema validation error.

Run the complete test suite

You now have tests across two files. Time to run everything together and see the full picture.

Before you run this, how many total test cases do you expect to see across both files?

  • Run the full suite across all test files:
pytest tests/ -v

You should see 17 tests passing. That is 12 from test_products.py (3 basic + 5 parametrized product IDs + 4 parametrized categories) plus 5 from test_negative.py (3 negative + 2 schema). All green.

What did schema validation add?

Your happy-path tests check that the API returns data. Your negative tests check that the API handles bad input. Schema validation checks the response contract itself.

If the API team renames price to cost tomorrow, your schema test fails immediately. That is the power of contract testing.

✔️ Awesome, I've got everything!

Great. Double check you have saved test_negative.py with all 5 test functions.

ⓧ I'd like to double check the full code

Here is the complete tests/test_negative.py file. Compare yours against it:

import pytest
from jsonschema import validate, ValidationError


PRODUCT_SCHEMA = {
    "type": "object",
    "required": ["id", "title", "price", "description", "category", "image"],
    "properties": {
        "id": {"type": "integer"},
        "title": {"type": "string"},
        "price": {"type": "number"},
        "description": {"type": "string"},
        "category": {"type": "string"},
        "image": {"type": "string"},
    },
}


def test_get_nonexistent_product(session, base_url):
    response = session.get(f"{base_url}/products/99999")
    assert response.status_code == 200

    product = response.json()
    assert product is None, "Expected null for nonexistent product ID"


def test_create_product_missing_fields(session, base_url):
    response = session.post(f"{base_url}/products", json={})
    data = response.json()
    assert "id" in data

And the second half:

def test_invalid_category(session, base_url):
    response = session.get(f"{base_url}/products/category/nonexistent")
    assert response.status_code == 200

    products = response.json()
    assert isinstance(products, list)
    assert len(products) == 0


def test_product_matches_schema(session, base_url):
    response = session.get(f"{base_url}/products/1")
    assert response.status_code == 200

    product = response.json()
    validate(instance=product, schema=PRODUCT_SCHEMA)


def test_all_products_match_schema(session, base_url):
    response = session.get(f"{base_url}/products")
    assert response.status_code == 200

    products = response.json()
    for product in products:
        validate(instance=product, schema=PRODUCT_SCHEMA)

Your test suite now catches bad input handling and contract violations automatically. Next up, you will extend this foundation with authentication flow testing in the Secret Mission.

Secret mission

Test the Authentication Flow

Your test suite covers products, categories, and error handling. But most real APIs require authentication. Can you test the full login flow, extract a JWT token, validate its structure, and chain authenticated requests together?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally and uses a free public API, so there are no ongoing costs.

Resources you used:

  • The ecommerce-qa project folder on your Desktop (includes virtual environment and all test files).
  • The FakeStore QA collection in Postman.

Keep everything running

No action needed. Choose this if you plan to keep building on your test suite or want to reference the code later.

  • Your ecommerce-qa folder stays on your Desktop with all test files intact.
  • Your Postman collection remains available for future exploratory testing.
  • The virtual environment stays deactivated until you reactivate it with venv\Scripts\activate (Windows) or source venv/bin/activate (macOS).

Pause - I'll come back to this later

Deactivate your virtual environment to free up your terminal session. Your files stay on disk for when you return.

  • In your terminal, run deactivate to exit the virtual environment.
  • Close the ecommerce-qa folder in Cursor.
  • When you return, reopen the folder in Cursor and reactivate the virtual environment.

Delete - I don't want to use this again

Remove all project files and the Postman collection entirely.

  • Deactivate the virtual environment by running deactivate in your terminal.
  • Close the ecommerce-qa folder in Cursor.
  • Delete the project folder by running this command (macOS/Linux):
rm -rf ~/Desktop/ecommerce-qa

On Windows, run this instead:

rmdir /s /q %USERPROFILE%\Desktop\ecommerce-qa
  • Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir %USERPROFILE%\Desktop (Windows). You should no longer see ecommerce-qa listed.
  • In Postman, right-click the FakeStore QA collection in the sidebar and select Delete.

Nice Work!

Nice Work!

You just built a professional QA test suite that validates a live ecommerce API across happy paths, error cases, and response contracts.

You've learned how to:

  • Build a Postman collection with scripted test assertions for exploratory API testing.
  • Write an automated Python test suite using pytest with parametrized data-driven tests that generate dozens of cases from a single function.
  • Add negative testing and JSON schema validation to catch error-handling gaps and contract violations automatically.
  • Secret Mission: Test the full authentication flow by logging in, extracting a JWT token, and verifying its structure.

Ready to quiz yourself?