简体   繁体   中英

pytest: How to force raising Exceptions during unit-testing?

In my python code, I am expecting exceptions could possibly be raised after calling method requests.Session.request() , for example these:

  • requests.exceptions.ConnectTimeout
  • requests.exceptions.ReadTimeout
  • requests.exceptions.Timeout

When any of these expected exceptions are raised, I handle them appropriately, for example possibly a retry situation.

My question, I am using py.test for unit testing, and I purposely want to inject raising exceptions from specific parts of my code. For example, the function that calls requests.Session.request() , instead of returning a valid requests.Response , it raises a requests.exception .

What I want to make sure that my code successfully handles expected and unexpected exceptions coming from other packages, which include those exceptions from requests .

Maybe... Is there a @decorator that I could add to the aforementioned function to raise exceptions upon request during unit testing?

Suggestions for doing exceptions injections for unit testing? (proper phrasing of my question would be greatly appreciated.)

Thanks for the responses!!!

Here is the entire singleton class that creates requests.Session and calls requests.Session.request() :

class MyRequest(metaclass=Singleton):

    def __init__(self, retry_tries=3, retry_backoff=0.1, retry_codes=None):
        self.session = requests.session()

        if retry_codes is None:
            retry_codes = set(REQUEST_RETRY_HTTP_STATUS_CODES)

        self.session.mount(
            'http',
            HTTPAdapter(
                max_retries=Retry(
                    total=retry_tries,
                    backoff_factor=retry_backoff,
                    status_forcelist=retry_codes,
                ),
            ),
        )

    def request(self, request_method, request_url, **kwargs):
        try:
            return self.session.request(method=request_method, url=request_url, **kwargs)
        except Exception as ex:
            log.warning(
                "Session Request: Failed: {}".format(get_exception_message(ex)),
                extra={
                    'request_method': request_method,
                    'request_url': request_url
                }
            )
            raise

You can make use of py.test raises, check it here: http://doc.pytest.org/en/latest/assert.html#assertions-about-expected-exceptions

Taking into account your code you could do something along the lines of the following:

from requests.exceptions import ConnectTimeout, ReadTimeout, Timeout
from unittest.mock import patch
import pytest

class TestRequestService:
   @patch('path_to_module.MyRequest')
   def test_custom_request(self, my_request_mock):
      my_request_mock.request.side_effect = ConnectTimeout

      with pytest.raises(ConnectTimeout):
          my_request_mock.request(Mock(), Mock())

Moreover, you could make use of pytest.parametrize( http://doc.pytest.org/en/latest/parametrize.html ) as well:

from requests.exceptions import ConnectTimeout, ReadTimeout, Timeout
from unittest.mock import patch
import pytest

class TestRequestService:
    @pytest.mark.parametrize("expected_exception", [ConnectTimeout, ReadTimeout, Timeout])
    @patch('path_to_module.MyRequest')
    def test_custom_request(self, my_request_mock, expected_exception):
        my_request_mock.request.side_effect = expected_exception
        with pytest.raises(expected_exception):
            my_request_mock.request(Mock(), Mock())

Here you can find some more examples about parametrize: http://layer0.authentise.com/pytest-and-parametrization.html

In my application I am catching exception requests.exceptions.ConnectionError and returning message which is in expected variable below. So the test looks like this:

import pytest
import requests

expected = {'error': 'cant connect to given url'}

class MockConnectionError:
    def __init__(self, *args, **kwargs):
        raise requests.exceptions.ConnectionError

def test_project_method(monkeypatch):
    monkeypatch.setattr("requests.get", MockConnectionError)
    response = project_method('http://some.url.com/')
    assert response == expected

Patching, mocking and dependecy-injection are techniques to inject fake objects. Patching is sometimes hard to do right, on the other hand dependency injection requires that have to change the code you want to test.

This is just a simple example how to use dependency-injection. First the code we want to test:

import requests
...  

def fetch_data(url, get=requests.get): 
    return get(url).json()

# this is how we use fetch_data in productive code:
answer = fetch_data("www.google.com?" + term)

And this is then the test:

import pytest

def test_fetch():

    def get_with_timeout(url):
        raise ConnectTimeout("message")

    with pytest.raises(ConnectTimeout) as e:
         # and now we inject the fake get method:
         fetch_data("https://google.com", get=get_with_timeout)

    assert e.value == "message"

In your example above, the mocking technique would be as follows:

def test_exception():

    class TimeoutSessionMock:

        def get(self, *args, **kwargs):
            raise ConnectTimeout("message")

    mr = MyRequest()
    mr.session = TimeoutSessionMock()

    with pytest.raises(ConnectTimeout) as e: 
         mr.request("get", "http://google.com")

    assert e.value == "message"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM