Published at

Pytest for API Testing: A Practical Guide

Pytest for API Testing: A Practical Guide

A comprehensive guide on using Pytest for API testing, including setup, test cases, fixtures, and examples.

Authors
  • avatar
    Name
    James Lau
    Twitter
  • Indie App Developer at Self-employed
Sharing is caring!
Table of Contents

This blog post provides a comprehensive guide on using Pytest for API testing, complete with practical examples. We’ll cover setting up Pytest, writing test cases, using fixtures for authentication, and testing various API endpoints.

Prerequisites

Before diving in, ensure you have the following:

  • Python 3.6+
  • pip package installer

Installation

Install Pytest and the pytest-django plugin using pip:

pip install pytest pytest-django

Configuration

Create a pytest.ini file in your project’s root directory to configure Pytest. This file tells Pytest how to find your Django settings and test files.

[pytest]
DJANGO_SETTINGS_MODULE = myapp.settings
# -- recommended but optional:
python_files = tests.py test_*.py *_tests.py
  • DJANGO_SETTINGS_MODULE: Specifies the Django settings module.
  • python_files: Defines the naming convention for test files.

Test Setup (test_server.py)

Let’s create a test_server.py file containing our test cases. We’ll use the requests library to make HTTP requests and Faker to generate fake data.

import pytest
import requests
import json
from faker import Faker

import sys
import os

# Add the parent directory to the Python path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from utils import get_fb_test_account_token

# Constants
BASE_URL = "https://your-web-app.net"  # You can change this or make it configurable
fake = Faker()

@pytest.fixture(scope="session")
def auth_token():
    # This function will be called once per test session
    # Implement your logic to get the auth token here
    # For example:
    auth_token = get_fb_test_account_token()
    print(f"auth_token {auth_token}")
    return auth_token

@pytest.fixture(scope="session")
def vendor_token():
    return "vendor-token"

def get_api_creator(BASE_URL, api, token):
    url = f"{BASE_URL}/{api}"
    headers = {"accept": "*/*", "Authorization": token}
    response = requests.get(url, headers=headers)
    return response

Explanation:

  • Imports: We import necessary libraries like pytest, requests, json, and Faker.
  • sys.path.append: This line is crucial if your utils module (containing helper functions like get_fb_test_account_token) resides in a parent directory. It modifies the Python path to include the parent directory, allowing the import to succeed. Adjust the path accordingly for your project structure.
  • BASE_URL: This constant stores the base URL of your API. It’s good practice to define such constants for easy modification.
  • auth_token fixture: This fixture is decorated with @pytest.fixture(scope="session"). This means the auth_token fixture will only be called once per test session. Fixtures are a powerful way to manage dependencies and setup/teardown resources for your tests. In this example, it retrieves an authentication token using the get_fb_test_account_token() function (implementation not shown but assumed to retrieve a token, perhaps from a testing service or environment variable). The token is then printed to the console for debugging and returned for use in subsequent tests.
  • vendor_token fixture: Similar to auth_token, this fixture provides a token for vendor-specific API calls.
  • get_api_creator function: This helper function simplifies making GET requests to the API. It takes the base URL, API endpoint, and authentication token as input and returns the response.

Writing Test Cases

Here are some example test cases demonstrating various API testing scenarios:

1. Test Firebase Authentication

def test_firebase_auth(auth_token):
    response = get_api_creator(BASE_URL, "api/test_firebase_auth", auth_token)
    assert response.status_code == 200
    data = response.json()
    assert "uid" in data

This test case calls an API endpoint (api/test_firebase_auth) that likely verifies Firebase authentication. It asserts that the response status code is 200 (OK) and that the JSON response contains a uid field.

2. Test Account Existence

def test_account_exist(auth_token):
    user_id = "test_user_id"  # You might want to get this from the firebase_auth test
    response = get_api_creator(BASE_URL, f"api/account/exist?userID={user_id}", auth_token)
    assert response.status_code == 200
    data = response.json()
    assert "exist" in data

This test checks if an account exists for a given userID. It calls the api/account/exist endpoint with the userID as a query parameter and asserts that the response contains an exist field.

3. Test Editing an Account

def test_edit_account(auth_token):
    name = fake.name()
    payload = {
        "name": name,
        "username": name,
        "bio": f"test_{name}",
    }
    response = requests.post(
        f"{BASE_URL}/api/account/edit",
        headers={"accept": "*/*", "Authorization": auth_token},
        files={'payload': (None, json.dumps(payload), 'application/json')}
    )
    assert response.status_code == 200

    # Verify the change
    response = get_api_creator(BASE_URL, "api/account", auth_token)
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == name

This test demonstrates how to use requests.post to send data to the API. It uses the Faker library to generate a random name and then sends a POST request to the api/account/edit endpoint to update the account information. Note the use of files parameter to send the JSON payload. Finally, it verifies that the name has been updated correctly by making a GET request to the api/account endpoint and checking the name field in the response.

4. Test Signup and Delete User

@pytest.fixture(scope="module")
def test_user_id():
    return "abcdefg9527952795279527"

@pytest.mark.order(1)
def test_signup(test_user_id):
    signup_param = {
        "name": "Alex Xiao2",
        "username": "Alex Xiao",
        "googleEmail": "9527@abcdefg.live",
        "userID": test_user_id,
    }
    response = requests.post(f"{BASE_URL}/api/signup", json=signup_param)
    assert response.status_code == 200

@pytest.mark.order(2)
def test_delete_user(auth_token):
    response = requests.delete(
        f"{BASE_URL}/api/account",
        headers={"accept": "*/*", "Authorization": auth_token}
    )
    assert response.status_code == 200

This example shows a signup and delete flow. test_user_id fixture provides a user ID. The @pytest.mark.order decorator can be used to specify the execution order of tests. In this case, test_signup is executed before test_delete_user. The test_signup function sends a POST request to the api/signup endpoint with signup parameters. The test_delete_user function sends a DELETE request to the api/account endpoint to delete the user.

5. Test Brand Rating and Following

def test_brand_rate_and_follow(auth_token):
    # Rate a brand
    response = requests.post(
        f"{BASE_URL}/api/brand/rate",
        headers={"accept": "*/*", "Authorization": auth_token},
        json={"company_id": 41, "rating": 5}
    )
    assert response.status_code == 200

    # Follow a brand
    response = requests.post(
        f"{BASE_URL}/api/brand/follow",
        headers={"accept": "*/*", "Authorization": auth_token},
        json={"company_id": 41}
    )
    assert response.status_code == 200

    # Unfollow a brand
    response = requests.delete(
        f"{BASE_URL}/api/brand/follow?company_id=41",
        headers={"accept": "*/*", "Authorization": auth_token}
    )
    assert response.status_code == 200

This test case demonstrates testing multiple API calls within a single test function. It rates a brand, follows a brand, and then unfollows the brand, asserting that each request is successful.

6. Test Getting Various API Endpoints

def test_new_build_get_apis(auth_token):
    endpoints = [
        "api/homepage",
        "api/account",
        "api/brand/all",
        "api/brand/favourites",
        "api/brand/detail?company_id=41",
        "api/brand/review?company_id=41&order=desc_time&filter=1-3",
        "api/notifications",
        "api/transactions",
        "api/vouchers",
        "api/help"
    ]
    for endpoint in endpoints:
        response = get_api_creator(BASE_URL, endpoint, auth_token)
        assert response.status_code == 200, f"Failed to get {BASE_URL}{endpoint}"

This test iterates through a list of API endpoints and asserts that each endpoint returns a successful response (status code 200). This is a useful test for ensuring that all basic endpoints are functioning correctly.

7. Test Brand Review Workflow

def test_brand_review(auth_token):
    # Create a review
    payload = {
        "company_id": 41,
        "rating": 3,
        "ambience_rating": 1,
        "quality_rating": 1,
        "service_rating": 1,
        "review_list": [
            {"question": "test_q", "answer": "test_a"},
            {"question": "test_q", "answer": "test_a"},
        ]
    }
    response = requests.post(
        f"{BASE_URL}/api/brand/review",
        headers={"accept": "*/*", "Authorization": auth_token},
        json=payload
    )
    assert response.status_code == 200
    review_data = response.json()
    review_id = review_data.get("review_id")  # Assuming the API returns the review ID

    # Upvote the review
    response = requests.post(
        f"{BASE_URL}/api/brand/review/upvote?brandreview_id={review_id}",
        headers={"accept": "*/*", "Authorization": auth_token}
    )
    assert response.status_code == 200

    # Remove upvote
    response = requests.delete(
        f"{BASE_URL}/api/brand/review/upvote?brandreview_id={review_id}",
        headers={"accept": "*/*", "Authorization": auth_token}
    )
    assert response.status_code == 200

    # Delete the review
    response = requests.delete(
        f"{BASE_URL}/api/brand/review?review_id={review_id}",
        headers={"accept": "*/*", "Authorization": auth_token}
    )
    assert response.status_code == 200

This test case tests the complete flow of creating, upvoting, and deleting a brand review.

8. Test Game API

def test_game_api(auth_token):
    # Initialize game
    response = requests.post(
        f"{BASE_URL}/api/game/initialize?discount_cnt=6&wheel_game=false",
        headers={"accept": "*/*", "Authorization": auth_token}
    )
    assert response.status_code == 200
    game_data = response.json()
    game_id = game_data["game_id"]

    # Finalize game
    data = {
        "game_id": game_id,
        "status_array": [False, False, False, False, False, False]
    }
    response = requests.post(
        f"{BASE_URL}/api/game/finalize",
        headers={"accept": "*/*", "Authorization": auth_token},
        json=data
    )
    assert response.status_code == 200

This test case initializes and finalizes a game, ensuring that the API endpoints for game management are working correctly.

9. Test Vendor APIs

def test_vendor_apis(vendor_token):
    endpoints = [
        "api/vendor/transactions",
        "api/vendor/polls",
        "api/vendor/help"
    ]
    for endpoint in endpoints:
        response = get_api_creator(BASE_URL, endpoint, vendor_token)
        assert response.status_code == 200

This test case checks vendor-specific API endpoints using the vendor_token fixture.

10. Test Payment Flow

def test_payment_flow(auth_token, vendor_token):
    # Generate QR code
    response = requests.post(
        f"{BASE_URL}/api/vendor/generate_payment_qrcode",
        headers={"accept": "*/*", "Authorization": vendor_token},
        json={"total_amount": 100}
    )
    assert response.status_code == 200
    qr_data = response.json()
    transaction_id = qr_data["transaction_id"]

    # Refresh QR code
    response = requests.post(
        f"{BASE_URL}/api/vendor/refresh_payment_qrcode?transaction_id={transaction_id}",
        headers={"accept": "*/*", "Authorization": vendor_token}
    )
    assert response.status_code == 200
    refresh_data = response.json()
    qr_code_token = refresh_data["token"]

    # Get transaction (vendor side)
    response = get_api_creator(BASE_URL, f"api/vendor/get-transaction?transaction_id={transaction_id}", vendor_token)
    assert response.status_code == 200
    assert response.json()["status"] == "AVAILABLE"

    # Get user payment method
    response = get_api_creator(BASE_URL, "api/stripe/paymentmethod", auth_token)
    assert response.status_code == 200

    # Scan QR code
    response = get_api_creator(BASE_URL, f"api/stripe/scan-transaction?token={qr_code_token}", auth_token)
    assert response.status_code == 200

    # Get transaction (user side)
    response = get_api_creator(BASE_URL, f"api/stripe/get-transaction?transaction_id={transaction_id}", auth_token)
    assert response.status_code == 200
    assert response.json()["status"] == "SCANNED"

    # Cancel transaction
    response = requests.post(
        f"{BASE_URL}/api/vendor/cancel-transaction?transaction_id={transaction_id}",
        headers={"accept": "*/*", "Authorization": vendor_token}
    )
    assert response.status_code == 200
    assert response.json()["msg"] == "success"

This comprehensive test case simulates a payment flow, including generating and refreshing QR codes, retrieving transaction details, and canceling the transaction. It also utilizes both auth_token and vendor_token fixtures.

Running Tests

To run the tests, navigate to your project’s root directory in the terminal and run:

pytest

Pytest will discover and execute all test functions in your test files, providing detailed output on the test results.

Conclusion

This blog post demonstrated how to use Pytest to create robust and comprehensive API tests. By utilizing fixtures, helper functions, and a clear test structure, you can ensure the reliability and functionality of your APIs.

Sharing is caring!