Fundamentals 11 min read

Python Interface Testing: Parameterization, Data‑Driven Tests, Deep/Shallow Copy, and Custom Assertions

This article explains how to perform Python API testing using parameterized tests, data‑driven tests with the ddt library, deep and shallow copying techniques, and custom assertion methods, providing code examples for each technique.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Python Interface Testing: Parameterization, Data‑Driven Tests, Deep/Shallow Copy, and Custom Assertions

In Python API testing, parameterized testing, data‑driven testing, and assertions are common techniques that improve test reuse and reduce code duplication.

Parameterized Testing passes varying inputs to a test function. Example using unittest :

import unittest
class TestMyAPI(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.base_url = "http://example.com/api"
    def test_get_user(self, user_id):
        url = f"{self.base_url}/users/{user_id}"
        response = requests.get(url)
        self.assertEqual(response.status_code, 200)  # assert status code 200
if __name__ == "__main__":
    suite = unittest.TestSuite()
    for user_id in [1, 2, 3]:
        suite.addTest(TestMyAPI("test_get_user", user_id=user_id))
    runner = unittest.TextTestRunner()
    runner.run(suite)

The example adds a user_id parameter to test_get_user and runs the test for several IDs.

Deep vs. Shallow Copy are important when handling configuration or response data. Use the copy module:

import copy
# Example response data
response_data = {
    "user": {"id": 1, "name": "Alice"},
    "items": [{"id": 1, "name": "Item 1"}]
}
# Shallow copy
copied_data1 = copy.copy(response_data)
# Deep copy
copied_data2 = copy.deepcopy(response_data)
# Modify copies
copied_data1["user"]["name"] = "Bob"
copied_data2["user"]["name"] = "Charlie"
print(response_data)  # Shallow copy changed original
print(response_data)  # Deep copy left original unchanged

Shallow copies share nested objects, while deep copies duplicate everything, preventing side‑effects across tests.

Data‑Driven Testing extracts test data from external sources. Using the ddt library:

pip install ddt
from ddt import ddt, data
import unittest
@ddt
class TestMyAPI(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.base_url = "http://example.com/api"
    @data((1, 200), (2, 200), (999, 404))
    def test_get_user(self, user_id, expected_status_code):
        url = f"{self.base_url}/users/{user_id}"
        response = requests.get(url)
        self.assertEqual(response.status_code, expected_status_code)
if __name__ == "__main__":
    unittest.main()

The @data decorator defines multiple (user_id, expected_status_code) tuples, creating separate test cases automatically.

Advanced uses of @data include multi‑scenario testing, boundary‑value analysis, error handling, performance data preparation, and environment switching. Example for login scenarios:

from ddt import ddt, data
import unittest
@ddt
class TestMyAPI(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.base_url = "http://example.com/api"
    @data(
        ({"username": "Alice", "password": "password123"}, 200),
        ({"username": "Bob", "password": "invalid_password"}, 401),
        ({"username": "", "password": ""}, 400),
        ({"username": None, "password": None}, 400),
    )
    def test_login(self, login_data, expected_status_code):
        url = f"{self.base_url}/login"
        response = requests.post(url, json=login_data)
        self.assertEqual(response.status_code, expected_status_code)

Assertions verify that actual results match expectations. Common unittest assertions include assertEqual , assertTrue , assertFalse , assertIn , and assertIsInstance . A custom assertion helper can improve readability:

class CustomAssertions:
    @staticmethod
    def assert_status_code(response, expected_status_code):
        assert response.status_code == expected_status_code, f"Expected status code {expected_status_code}, got {response.status_code}"
    @staticmethod
    def assert_json_contains(response, key, value):
        json_data = response.json()
        assert key in json_data, f"Key '{key}' not found in JSON response"
        assert json_data[key] == value, f"Value of key '{key}' is {json_data[key]}, expected {value}"
    @staticmethod
    def assert_json_keys_exist(response, keys):
        json_data = response.json()
        for key in keys:
            assert key in json_data, f"Key '{key}' not found in JSON response"
from unittest import TestCase
import requests
class MyTestCase(TestCase):
    def test_my_api(self):
        response = requests.get("http://example.com/api")
        CustomAssertions.assert_status_code(response, 200)
        CustomAssertions.assert_json_contains(response, "name", "John Doe")
        CustomAssertions.assert_json_keys_exist(response, ["name", "email", "age"])

Encapsulating assertions in a utility class makes test code clearer, more maintainable, and reusable across multiple test cases.

Testingdata-drivenparameterizationAssertions
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.