Модульный тест Python для блока Try/Except

У меня есть следующая функция с модульными тестами:

#!/usr/bin/env python3
# https://docs.python.org/3/library/ipaddress.html
# https://docs.python.org/3.4/library/unittest.html

import ipaddress
import unittest

from unittest.mock import patch
from unittest import TestCase

def validate_IP():
    """Prompt user for IPv4 address, then validate."""

    while True:
        try:
            return ipaddress.IPv4Address(input('Enter a valid IPv4 address: '))
        except ValueError:
            print('Bad value, try again.')


class validate_IP_Test(unittest.TestCase):

    @patch('builtins.input', return_value='192.168.1.1')
    def test_validate_IP_01(self, input):
        self.assertIsInstance(validate_IP(), ipaddress.IPv4Address)

    @patch('builtins.input', return_value='10.0.0.1')
    def test_validate_IP_02(self, input):
        self.assertIsInstance(validate_IP(), ipaddress.IPv4Address)

    @patch('builtins.input', return_value='Derp!')
    def test_validate_IP_03(self, input):
        self.assertRaises(ValueError, msg=none)


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

Функция использует модуль ipaddress в Python3 для проверки ввода пользователя, т. е. проверяет, что ввод пользователя является фактическим IPv4 address. Мои первые два теста работают, как и ожидалось. Однако я не совсем понимаю, как проверить недопустимый ввод с помощью модуля Python3 unittest для части исключения функции, как в третьем тесте.

Когда вводится неверный ввод, тест должен распознать, что было создано исключение, и пройти тест. Для справки, вот соответствующий вывод интерпретатора, когда я ввожу неверный адрес:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File 
raise AddressValueError("Expected 4 octets in %r" % ip_str)
ipaddress.AddressValueError: Expected 4 octets in 'derp'`

person marshki    schedule 17.09.2019    source источник
comment
Видимое поведение при неверном вводе заключается в том, что запрашивается новый ввод. Тот факт, что это делается с помощью перехвата исключения, является деталью реализации, которую вы должны иметь возможность изменить, не нарушая тесты.   -  person Stop harming Monica    schedule 18.09.2019


Ответы (3)


Вы можете использовать метод assertRaises в качестве менеджера контекста:

@patch('builtins.input', return_value='Derp!')
def test_validate_IP_03(self, input):
    with self.assertRaises(ValueError):
        validate_IP()

Однако ваша функция validate_IP перехватывает само исключение в бесконечном цикле, поэтому приведенный выше тест на самом деле не пройдет. Вы можете сделать так, чтобы он не повторял попытку, а вместо этого повторно вызывал исключение после вывода сообщения об ошибке, если вы хотите перехватить исключение вне вызова:

def validate_IP():
    try:
        return ipaddress.IPv4Address(input('Enter a valid IPv4 address: '))
    except ValueError:
        print('Bad IPv4 address.')
        raise
person blhsing    schedule 17.09.2019
comment
Ваш первый пример - это один тест, который я пытался запустить, хотя, как вы упомянули, я попал в бесконечный цикл. Ваш второй пример работает в соответствии с моим намерением. Спасибо. - person marshki; 17.09.2019

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

 def validate_IP():
    """Prompt user for IPv4 address, then validate."""

    while True:
        try:
            return ipaddress.IPv4Address(input('Enter a valid IPv4 address: '))
        except ValueError:
            print('Bad value, try again.')
            raise

Это заставит вас выйти из этого while, что сделает его бессмысленным. Я бы переместил это время в другой метод или функцию, чтобы вы могли протестировать только поведение повышения IPv4Address.

Другая проблема заключается в вызове input внутри функции, что очень раздражает при тестировании. Я бы выбрал def validate_ip(ip):, гораздо проще проверить.

С уважением,

person geckos    schedule 17.09.2019

Видимое поведение при неверном вводе заключается в том, что запрашивается новый ввод. Тот факт, что это делается с помощью перехвата исключения, является деталью реализации, на которую лучше не обращать внимания тестам, чтобы вы могли изменить эту деталь реализации, не нарушая тесты. Трассировка в вашем вопросе - это то, что будет делать IPv4Address, но не то, что будет делать validate_IP().

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

def validate_IP():
    """Prompt user for IPv4 address, then validate."""

    while True:
        try:
            return ipaddress.IPv4Address(input('Enter a valid IPv4 address: '))
        except ipaddress.AddressValueError:
            print('Bad value, try again.')

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

@patch('builtins.input', side_effect=('Derp!', '10.0.0.1'))
def test_invalid_IP(self, input):
    self.assertEqual(validate_IP(), ipaddress.IPv4Address('10.0.0.1'))

Кстати, я предлагаю вам переосмыслить имена, я думаю, что validate_IP на самом деле не говорит, что делает функция, а имена тестов в значительной степени бессмысленны.

person Stop harming Monica    schedule 17.09.2019