Search code examples
pythonapitestingpytest

Create tests to cover cases where the response from the third-party API is not the expected response


I have an API on Flask (Python) that interacts with a third-party API, for example, creating documents.
My code:

def my_function(data):
    response = requests.post("https://some_api.com", json=data)
    json_response = response.json()
    if json_response["message"] == "Successfully created":
        return "Successfully created"
    elif json_response["message"] == "Already exists":
        return "Already exists"
    elif json_response["message"] == "Wrong data":
        return "Wrong data"
    else:
        return json_response["message"]

I have already created tests to cover cases where the third-party API works as expected(where "message" equals to "Successfully created", "Already exists" or "Wrong data").
However, I also want to cover case where the third-party API does not work as expected. I want to create a test that covers the 'else' branch, but I cannot reach it while the third-party API is working properly.
Is there a way to obtain a response that the third-party API would not normally provide?
Can anyone suggest how I can achieve this?

Thank you in advance.


Solution

  • You would typically use a mock to replace your interaction with the actual API with synthesized behavior that you control.

    For example, in the following code we're using mock.patch("requests.post") (where mock.patch comes from the unittest module) so that calling requests.post doesn't actually make an HTTP request to a remote site; instead requests.post -- while inside our test function -- is a fake object for which we can control the return value and other aspects:

    import pytest
    import requests
    from unittest import mock
    
    
    def my_function(data):
        response = requests.post("https://some_api.com", json=data)
        json_response = response.json()
        if json_response["message"] == "Successfully created":
            return "Successfully created"
        elif json_response["message"] == "Already exists":
            return "Already exists"
        elif json_response["message"] == "Wrong data":
            return "Wrong data"
        else:
            return json_response["message"]
    
    
    @mock.patch("requests.post")
    @pytest.mark.parametrize(
        "json_response,expected_return",
        [
            ({"message": "Successfully created"}, "Successfully created"),
            ({"message": "Already exists"}, "Already exists"),
            ({"message": "Wrong data"}, "Wrong data"),
            ({"message": "Some other message"}, "Some other message"),
        ],
    )
    def test_my_function_suceeds(mock_post, json_response, expected_return):
        mock_response = mock.MagicMock()
        mock_response.json.return_value = json_response
        mock_post.return_value = mock_response
        res = my_function("")
        assert res == expected_return
    

    This is a parametrized test, meaning that while we have a single test function, pytest will run it multiple times for each set of parameters. Running this with pytest -v yields:

    ============================= test session starts ==============================
    platform linux -- Python 3.11.1, pytest-7.2.1, pluggy-1.0.0 -- /home/lars/.local/share/virtualenvs/python-LD_ZK5QN/bin/python
    cachedir: .pytest_cache
    rootdir: /home/lars/tmp/python
    plugins: anyio-3.6.2, base-url-2.0.0, playwright-0.3.0
    collecting ... collected 4 items
    
    testex.py::test_my_function_suceeds[json_response0-Successfully created] PASSED [ 25%]
    testex.py::test_my_function_suceeds[json_response1-Already exists] PASSED [ 50%]
    testex.py::test_my_function_suceeds[json_response2-Wrong data] PASSED    [ 75%]
    testex.py::test_my_function_suceeds[json_response3-Some other message] PASSED [100%]
    ============================== 4 passed in 0.04s ===============================
    

    This is actually a bit more complicated than necessary, since in all cases your my_function function is simply returning the value of the message key, so we could in fact have written our test like this:

    @mock.patch("requests.post")
    @pytest.mark.parametrize(
        "message",
        [
            "Successfully created",
            "Already exists",
            "Wrong data",
            "Some other message",
        ],
    )
    def test_my_function_suceeds(mock_post, message):
        mock_response = mock.MagicMock()
        mock_response.json.return_value = {"message": message}
        mock_post.return_value = mock_response
        res = my_function("")
        assert res == message
    

    But I think the first example is more representative of how you would normally write this, since it's unlikely most functions would have such trivial behavior.