How to run API integration tests

As with any software system, you need to test how APIs interact with each other and within your system to ensure they’re exchanging data correctly and reliably—which is where API integration testing comes into play.

In this tutorial, you'll learn what API integration testing is, why it's important, how to perform it (using an example), and some best practices to keep in mind when doing so.

{{blog-cta-100+}}

What is API integration testing?

API integration testing is the process of testing your integration through making sure that every component of the integration will work as intended with the API. There are many components to integration testing, including the API authentication, pagination, rate limiting, and response bodies.

Related: A guide to API integration tools

What does API integration testing solve?

API integration testing is a crucial part of the development process, and prevents a lot of heavy-duty bandaid work down the road.

Here's some benefits fromAPI integration testing:

  • Ensure reliability, data accuracy, and stability: Testing ensures that your integrations are reliable and can handle the expected load (i.e offers stability). It also verifies the accuracy of the data that’s being exchanged between systems.
  • Improve error handling: API integrations can be complex and prone to errors. Testing not only helps identify and fix these errors but also improves the error handling within the code that deals with error responses from the APIs.
  • Check performance: Testing allows you to detect performance bottlenecks and optimize the integrations while also considering relevant API rate limits.
  • Maintain compatibility: APIs and systems can change over time. Testing ensures that updates in one system remain compatible with all the other systems involved, which maintains seamless communication.
  • Improve security: Thorough testing can help identify security vulnerabilities, ensuring that data exchanges between systems remain safe and protected.

Related: The challenges of building secure integrations

API integration testing versus unit testing

Unit testing is where the smallest piece of code, or unit, is tested individually. Integration testing is conducted separately and combines the modules of unit testing into a single test to confirm the functionality of an entire integration.

At Merge, we add unit tests for every backend change, and we layer on integration tests for every integration we build. Whenever we make a change to the integration, it may not require us to make unit test modifications, but it absolutely will require integration test modifications.

Key steps in API integration testing

Here are recommended steps for API integration testing:

  1. Come up with testing plan for your integrations
  2. Build varied cases and use cases to test
  3. Run tests on the integration
  4. Track, report, and resolve any errors that come up from those tests
  5. Retest the integration
  6. Repeat this process until the integration no longer runs into bugs

Best practices for testing API integrations

Here are some general best practices that can help you develop an effective API integration testing plan:

Choose a network request mocking library when possible

You should try to use specialized network request mocking libraries (such as requests_mock) for API integration tests. These libraries simulate real network interactions more accurately. They also provide the ability to isolate testing scenarios, reduce dependency on external APIs, simulate error conditions for comprehensive testing (as demonstrated in the TestAPIIntegrations class), and improve testing speed by eliminating actual network requests. Additionally, these libraries enhance the reliability and efficiency of API integration tests, especially for tests that involve numerous network requests.

Evaluate error handling implementations

You should cover various error scenarios during API interactions to ensure that your system can handle them without crashing. For example, in the API integration tests earlier, the requests_mock code simulated an error response from the API: <code class="blog_inline-code">mock.get("<api-url>", status_code=500)</code>

This part tested whether the get_product_availability(...) code correctly handles API errors without crashing. Depending on your specific implementation, you can simulate various HTTP errors (such as HTTP 404 or HTTP 401) to verify if the corresponding code appropriately handles them.

Focus on boundary values

Test extreme input values to identify unexpected behaviors and ensure that your system can gracefully handle these inputs. For instance, you can test the get_converted_price(...) function by providing extremely low or high values for product prices. This verifies if the code appropriately handles such values, including displaying helpful error messages for invalid inputs.

Test for backward compatibility

Conduct tests to verify that modifications to the third-party APIs don't disrupt existing functionality.

For instance, imagine you have an application that relies on the Google Maps API for location data. When Google Maps makes changes to its APIs, you need to modify your API integration code and the associated tests to mock the modified API responses and parameters, as required. You also have to perform backward compatibility testing to make sure that the rest of your system remains functional with the latest API changes. If something breaks with those changes, you need to fix that as well. This prevents any disruptions for existing users.

Use realistic data

Create tests using real-world data scenarios. This guarantees that your system can adeptly manage real user interactions.

For instance, when simulating user profile responses from an authentication API, use realistic names that contain Unicode characters to validate your system's ability to manage these names in both code and databases.

Another example is to use an accurate USD-INR conversion rate, such as 82.6, instead of simplified 82 (an integer) to ensure that the corresponding code can handle decimal values correctly.

Keep tests updated

Keep your tests up-to-date with evolving APIs so that they remain reliable indicators of your system's functionality. As your codebase evolves, new features, improvements, or bug fixes may alter the behavior of your system. Failing to update tests can lead to false positives or negatives, where tests pass even though the system isn't functioning as intended or tests fail due to changes you intentionally made.

Related: A guide to integrating REST APIs

Example of API integration testing

The following Python code demonstrates the integration of two public APIs. The first API handles currency price conversion, and the second API provides information for a given zip code in the U.S.

To understand the process of integrating these APIs and conducting integration tests, follow these steps:

Install the required libraries

You can create a new virtual environment and activate it before installing the Python libraries mentioned later. This allows you to isolate your project and avoid any dependency conflicts.

This code requires that you install the libraries requests and requests_mock, and you can install them using a pip like the following: <code class="blog_inline-code">pip install requests requests_mock</code>

Integrate external APIs

After you've installed the required libraries, you need to create a new file named api_integration.py and paste the following code into it:


import requests


def get_converted_price(product_price: int, conversion_currency: str) -> float:
    converted_price = None
    base_currency = "usd"
    api_url = f"https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/{base_currency}/{conversion_currency.lower()}.json"

    try:
        resp = requests.get(api_url)
        if resp.ok:     # HTTP OK
            currency_data = resp.json()
            converted_price = product_price * currency_data[conversion_currency]
            print(f"Converted price for the product: {round(converted_price, 2)} {conversion_currency.upper()}")
        else:
            print(f"Response: {resp.text} (HTTP-{resp.status_code})")
    except Exception as ex:
        print(f"Error: {ex}")    # show error
    finally:
        return converted_price


def get_product_availability(zipcode: int) -> bool:
    availability = None
    skip_states = ["Idaho", "Indiana", "Kansas", "Kentucky", "Mississippi", "Nebraska", "North Carolina",
                   "Oklahoma", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Virginia", "Wyoming"]
    api_url = f"https://api.zippopotam.us/us/{zipcode}"

    try:
        resp = requests.get(api_url)
        if resp.ok:     # HTTP OK
            zip_data = resp.json()
            state = zip_data["places"][0]["state"]
            availability = False if state in skip_states else True
            print(f"The product is available in: {state} - {availability}")
        else:
            print(f"Response: {resp.text} (HTTP-{resp.status_code})")
    except Exception as ex:
        print(f"Error: {ex}")    # show error
    finally:
        return availability
        

The first function, get_converted_price(...), integrates the currency conversion API. This function requires two parameters: product_price and conversion_currency. The former denotes the price in the base currency (USD), while the latter indicates the local currency to which the product price should be converted.

The code obtains the most recent USD conversion rate for the specified currency via the currency conversion API using this call: requests.get(api_url). This conversion rate is then used to compute the relevant product price, which is returned by the function. For instance, similar converted prices are often displayed when purchasing domains.

The second function, get_product_availability(...), integrates the zip code information API. This function takes a single parameter: zipcode of the location to determine product availability (limited to US zip codes). Additionally, this function maintains a list of states where the product is not available: skip_states.

Similar to the previous function, the code retrieves location data via the zip code information API. It extracts the state associated with the given zip code from this data. If this extracted state is among those listed in skip_states, the function returns False; otherwise, it returns True.

Both these functions handle unexpected errors using a try-except-finally block. Moreover, they print their computed results before returning them, making it easier to examine the output while testing these API integrations.

Related: What is API integration management? Here's what you need to know

Test the API integrations

API integration testing utilizes the requests-mock library, specifically designed for mocking HTTP requests. It allows you to mock different HTTP methods and responses without actually sending the requests to the real server. In addition, the built-in Python module unittest provides a standard testing framework.

The class TestAPIIntegrations is subsequently displayed and includes five tests for the previously-discussed functions (i.e. get_converted_price(...) and get_product_availability(...)). You can copy and paste this code into a new file: api_integration_tests.py:


import unittest
import requests_mock
from api_integration import get_converted_price, get_product_availability


class TestAPIIntegrations(unittest.TestCase):

    def test_get_converted_price(self):
        test_data = {"date": "2023-09-01", "inr": 82.6}
        expected_price = 8260
        with requests_mock.Mocker() as mock:
            mock.get("https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/usd/inr.json", json=test_data)
            calculated_price = get_converted_price(100, "inr")
            self.assertEqual(calculated_price, expected_price, f"Price calculation is incorrect.")

    def test_get_converted_price_failure(self):
        with requests_mock.Mocker() as mock:
            mock.get("https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies/usd/inr.json", status_code=404)
            calculated_price = get_converted_price(100, "inr")
            self.assertIsNone(calculated_price, "Price is _not_ None.")

    def test_get_product_availability_true(self):
        test_data = {"post code": "90210", "places": [{"place name": "Beverly Hills", "state": "California"}]}
        with requests_mock.Mocker() as mock:
            mock.get("https://api.zippopotam.us/us/90210", json=test_data)
            product_availability = get_product_availability(90210)
            self.assertTrue(product_availability, f"Product availability is incorrect.")

    def test_get_product_availability_false(self):
        test_data = {"post code": "75001", "places": [{"place name": "Addison", "state": "Texas"}]}
        with requests_mock.Mocker() as mock:
            mock.get("https://api.zippopotam.us/us/75001", json=test_data)
            product_availability = get_product_availability(75001)
            self.assertFalse(product_availability, f"Product availability is incorrect.")

    def test_get_product_availability_failure(self):
        with requests_mock.Mocker() as mock:
            mock.get("https://api.zippopotam.us/us/75001", status_code=500)
            product_availability = get_product_availability(75001)
            self.assertIsNone(product_availability, f"Product availability is incorrect.")


if __name__ == '__main__':
    unittest.main(verbosity=2)
    

Now, examine this code for test_get_converted_price(). Here, the requests_mock library is used within the context of the with statement to temporarily replace the behavior of the requests.get(...) method. The mock is set up to respond with specific JSON data (test_data) whenever a GET request is directed at the corresponding API.

When the get_converted_price(...) function is called during the test, it interacts with the mock instead of making an actual network request. Consequently, the function calculates the price using the conversion rate provided by this test data:

{"date": "2023-09-01", "inr": 82.6}

In the end, the expected price, which is 8,260 (100 âś• 82.6), is compared to the calculated price returned by the get_converted_price(...) function using the mock API response: <code class="blog_inline-code">self.assertEqual(calculated_price, expected_price, f"Price calculation is incorrect.")</code>

The corresponding message is displayed only if the assertion fails.

Utilizing requests_mock in the test allows you to verify that the code within the function get_converted_price(...) behaves as expected using test_data that simulates the API response.

Now, consider the code for test_get_converted_price_failure(). This test emulates an API failure by simulating an HTTP-404 response, as shown in this code snippet: <code class="blog_inline-code">mock.get("<api-url>", status_code=404)</code>

This test verifies that the get_converted_price(...) function correctly returns None: <code class="blog_inline-code">self.assertIsNone(calculated_price, "Price is _not_ None.")</code>

Similarly, these subsequent tests validate the functionality of the get_product_availability(...) function:

  • <code class="blog_inline-code">test_get_product_availability_true()</code> checks whether the function correctly returns True for the given zip code where the product is available. It simulates this API response:

{"post code": "90210", "places": [{"place name": "Beverly Hills", "state": "California"}]}
  • <code class="blog_inline-code">test_get_product_availability_false()</code>ensures that the function properly returns False for the provided zip code where the product is not available. It emulates this API response:
 
```json
{"post code": "75001", "places": [{"place name": "Addison", "state": "Texas"}]}
```
  • <code class="blog_inline-code">test_get_product_availability_failure()</code> verifies that the function correctly returns None when an API failure occurs. In this scenario, it simulates an HTTP-500 response:

mock.get("", status_code=500)

These tests cover a few possible scenarios. More tests can be added to test specific scenarios (such as extreme product prices or non-U.S. zip codes) as required.

It's crucial to understand that any change in the API response format, for whatever reason, requires modifications not just in the original code but also in the mock responses of such tests.

Related: Examples of API integrations

Run API integration tests

The following code snippet runs all the tests discussed previously:<code class="blog_inline-code">if __name__ == '__main__':    unittest.main(verbosity=2)</code>

The parameter verbosity=2 ensures that these tests generate detailed output when they are executed, as shown here:


$ python api_integration_tests.py
test_get_converted_price (__main__.TestAPIIntegrations) ... Converted price for the product: 8260.0 INR
ok
test_get_converted_price_failure (__main__.TestAPIIntegrations) ... Response:  (HTTP-404)
ok
test_get_product_availability_failure (__main__.TestAPIIntegrations) ... Response:  (HTTP-500)
ok
test_get_product_availability_false (__main__.TestAPIIntegrations) ... The product is available in: Texas - False
ok
test_get_product_availability_true (__main__.TestAPIIntegrations) ... The product is available in: California - True
ok

----------------------------------------------------------------------
Ran 5 tests in 0.324s

OK

This test output also includes print(...) statements from the functions get_converted_price(...) and get_product_availability(...), showing their results and error-handling.

The complete code for this tutorial is available in this GitHub repository.

Final thoughts

As you can tell from our examples, testing API integrations in-house can be labor-intensive—especially when there are several integrations that need to be tested over time.

If your team is looking to implement product integrations and doesn’t want to handle all of the tedious, time-consuming tasks that come with building and maintaining them—like testing—, you can leverage Merge.

Merge offers a unified API that lets you connect to hundreds of applications automatically, from CRM solutions to file storage tools to HRIS platforms.

To learn more about Merge and to discover how it can help transform your product integration strategy, you can schedule a demo with one of our integration experts.