Как я могу использовать unittest.mock для удаления побочных эффектов из кода?

#python #unit-testing #python-3.x #exception-handling #python-unittest.mock

#python #модульное тестирование #python-3.x #обработка исключений #python-unittest.mock

Вопрос:

У меня есть функция с несколькими точками отказа:

 def setup_foo(creds):
    """
    Creates a foo instance with which we can leverage the Foo virtualization
    platform.

    :param creds: A dictionary containing the authorization url, username,
                  password, and version associated with the Foo
                  cluster.
    :type creds:  dict
    """

    try:
        foo = Foo(version=creds['VERSION'],
                  username=creds['USERNAME'],
                  password=creds['PASSWORD'],
                  auth_url=creds['AUTH_URL'])

        foo.authenticate()
        return foo
    except (OSError, NotFound, ClientException) as e:
        raise UnreachableEndpoint("Couldn't find auth_url {0}".format(creds['AUTH_URL']))
    except Unauthorized as e:
        raise UnauthorizedUser("Wrong username or password.")
    except UnsupportedVersion as e:
        raise Unsupported("We only support Foo API with major version 2")
  

и я хотел бы проверить, что все соответствующие исключения перехвачены (хотя и не обрабатываются должным образом в настоящее время).

У меня есть начальный тестовый пример, который проходит:

 def test_setup_foo_failing_auth_url_endpoint_does_not_exist(self):
    dummy_creds = {
        'AUTH_URL' : 'http://bogus.example.com/v2.0',
        'USERNAME' : '', #intentionally blank.
        'PASSWORD' : '', #intentionally blank.
        'VERSION'  : 2 
    }
    with self.assertRaises(UnreachableEndpoint):
        foo = osu.setup_foo(dummy_creds)
  

но как я могу заставить мою тестовую среду поверить, что AUTH_URL на самом деле является допустимым / доступным URL?

Я создал макет класса для Foo :

 class MockFoo(Foo):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
  

и моя мысль заключается в том, чтобы имитировать вызов setup_foo и устранить побочный эффект создания UnreachableEndpoint исключения. Я знаю, как добавлять побочные эффекты в Mock с unittest.mock помощью, но как я могу их удалить?

Комментарии:

1. Вы хотите протестировать другие исключения, поэтому в ваших тестах не имеет значения, является ли это на самом деле реальным URL. Что вы хотите знать, так это то, как ведет себя ваш код, когда он вызывает исключения, которые вы хотите перехватить, поэтому просто заставьте вашего контекстного менеджера вызвать исключение, которое вы хотите протестировать. В конечном счете, просто повторно используйте тот же метод, но для каждого из разных исключений и протестируйте, чтобы убедиться, что ваш код ведет себя в отношении тех исключений, которые вызовет клиент

2. @idjaw Что делать, если исключения генерируются в определенном порядке; т.Е. Сначала проверьте, действительный ли URL (выбросить, если нет), затем проверьте, авторизован ли (выбросить, если нет) и т.д.?

3. Однако почему порядок имеет значение? если в данном случае использования возникает исключение, ваш код хочет перехватить его в данный момент времени и вести себя соответствующим образом.

4. @idjaw Это именно мое намерение — имитировать вариант использования, в котором URL-адрес действителен, а другие учетные данные — нет.

5. Было бы лучше, если бы я проиллюстрировал это для вас в ответе. Я понимаю, что вы пытаетесь сделать сейчас.

Ответ №1:

Предполагая, что ваши исключения создаются из foo.authenticate() , вы хотите понять, что здесь не обязательно имеет значение, действительно ли данные действительно действительны в ваших тестах. На самом деле вы пытаетесь сказать следующее:

Когда этот внешний метод вызывает что-то, мой код должен вести себя соответствующим образом на основе этого чего-то.

Итак, имея это в виду, вы хотите использовать разные методы тестирования, в которых вы передаете то, что должно быть достоверными данными, и ваш код реагирует соответствующим образом. Сами данные не имеют значения, но они предоставляют документированный способ показать, как код должен вести себя с данными, которые передаются таким образом.

В конечном счете, вас не должно волновать, как клиент nova обрабатывает данные, которые вы ему предоставляете (клиент nova протестирован, и вас это не должно волновать). Что вас волнует, так это то, что он плюет на вас и как вы хотите с этим справиться, независимо от того, что вы ему дали.

Другими словами, ради ваших тестов вы можете фактически передать фиктивный URL как:

 "this_is_a_dummy_url_that_works"
  

Ради ваших тестов вы можете пропустить это, потому что в вашем mock случае вы будете повышать соответственно.

Например. То, что вы должны здесь делать, на самом деле является издевательством Client novaclient . Теперь, имея этот макет, вы можете управлять любым вызовом в novaclient, чтобы вы могли правильно протестировать свой код.

Это фактически подводит нас к корню вашей проблемы. Ваше первое исключение — это обнаружение следующего:

 except (OSError, NotFound, ClientException)
  

Проблема здесь в том, что вы сейчас улавливаете ClientException . Почти каждое исключение в novaclient наследуется от ClientException , поэтому независимо от того, что вы пытаетесь протестировать за пределами этой строки исключения, вы никогда не достигнете этих исключений. Здесь у вас есть два варианта. Перехватите ClientException и просто создайте пользовательское исключение или, remote ClientException , и будьте более явными (как вы уже делаете).

Итак, давайте начнем с удаления ClientException и соответствующим образом настроим наш пример.

Итак, в вашем реальном коде вы должны теперь установить свою первую строку исключения как:

 except (OSError, NotFound) as e:
  

Кроме того, следующая проблема, с которой вы сталкиваетесь, заключается в том, что вы неправильно издеваетесь. Предполагается, что вы должны издеваться над тем, где вы тестируете. Итак, если ваш setup_nova метод находится в вызываемом модуле your_nova_module . Именно в отношении этого вы должны издеваться. Приведенный ниже пример иллюстрирует все это.

 @patch("your_nova_module.Client", return_value=Mock())
def test_setup_nova_failing_unauthorized_user(self, mock_client):
    dummy_creds = {
        'AUTH_URL': 'this_url_is_valid',
        'USERNAME': 'my_bad_user. this should fail',
        'PASSWORD': 'bad_pass_but_it_does_not_matter_what_this_is',
        'VERSION': '2.1',
        'PROJECT_ID': 'does_not_matter'
    }

    mock_nova_client = mock_client.return_value
    mock_nova_client.authenticate.side_effect = Unauthorized(401)

    with self.assertRaises(UnauthorizedUser):
        setup_nova(dummy_creds)
  

Итак, основная идея приведенного выше примера заключается в том, что не имеет значения, какие данные вы передаете. Что действительно важно, так это то, что вы хотите знать, как ваш код будет реагировать при вызове внешнего метода.

Итак, наша цель здесь — создать что-то, что заставит протестировать ваш второй обработчик исключений: Unauthorized

Этот код был протестирован против кода, который вы опубликовали в своем вопросе. Единственные изменения, которые были внесены, касались имен модулей, отражающих мою среду.

Комментарии:

1. клиент nova протестирован, и вас это не должно волновать, хахахахааааааа … 🙂 Спасибо за такой отличный ответ!

2. хе-хе 🙂 Просто чтобы уточнить, что, говоря «безразлично», я имел в виду в отношении ваших собственных unittests, вы предполагаете, что novaclient обработает все, что вы ему дадите, как ожидалось, и вы имеете дело с тем, что он должен вернуть.

3. Черт возьми, я использую novaclient напрямую, но хотел сделать свой вопрос более независимым от платформы. 🙂 novaclient.v2.client.Client имеет authenticate метод.

4. @erip Ты прав. Я только что проверил и на самом деле меняю один из своих методов на использование authenticate метода вместо этого. Что, в свою очередь, означает, что я собираюсь обновить свои unittests для случая использования authenticate now. 🙂

5. Это может быть глупый вопрос, но как бы я определил authenticate_mock ? Я бы предположил, что я мог бы просто объявить authenticate_mock=foo.authenticate с @patch(foo.Foo.authenticate) помощью , но когда я делаю это, я обнаруживаю, что мои утверждения не выполняются.

Ответ №2:

Если вы хотите имитировать http-серверы из поддельных URL-адресов, я предлагаю вам проверить HTTPretty . Он имитирует URL-адреса на уровне сокета, поэтому может обмануть большинство HTTP-библиотек Python, что это допустимый URL.

Я предлагаю следующую настройку для вашего unittest:

 class FooTest(unittest.TestCase):
    def setUp(self):
        httpretty.register_uri(httpretty.GET, "http://bogus.example.com/v2.0",
                       body='[{"response": "Valid"}]',
                       content_type="application/json")
    @httpretty.activate
    def test_test_case(self):
        resp = requests.get("http://bogus.example.com/v2.0")
        self.assertEquals(resp.status_code, 200)
  

Обратите внимание, что макет будет применяться только к стекам, которые оформлены с http.activate помощью декоратора, поэтому он не просочится в другие места вашего кода, которые вы не хотите имитировать. Надеюсь, это имеет смысл.