4

I have the following function with unit tests:

#!/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()

The function uses the ipaddress module in Python3 to validate user input, i.e. checks that user input is an actual IPv4 address. My first two tests work as expected. I'm not clear, though, on how to test for invalid input using Python3's unittest module for the exception portion of the function, as in the third test.

When invalid input is entered, the test should recognize that an exception was thrown and pass the test. For reference, here's the relevant output from an interpreter when I enter an invalid address:

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'`
marshki
  • 175
  • 1
  • 11
  • The visible behavior on invalid input is that a new input is requested. The fact that it is done by means of catching an exception is an implementation detail that you should be able to change without breaking the tests. – Stop harming Monica Sep 17 '19 at 21:07

3 Answers3

4

You can use the assertRaises method as a context manager:

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

However, your validate_IP function catches the exception itself within an infinite loop so the above test won't actually pass. You can make it not retry, and re-raise the exception after outputting the error message instead, if you intend to be able to catch the exception outside the call:

def validate_IP():
    try:
        return ipaddress.IPv4Address(input('Enter a valid IPv4 address: '))
    except ValueError:
        print('Bad IPv4 address.')
        raise
blhsing
  • 91,368
  • 6
  • 71
  • 106
  • 1
    Your first example is one test I tried to run, though, as you mentioned, I got caught in an infinite loop. Your second example works per my intention. Thanks. – marshki Sep 17 '19 at 18:21
1

The exception is not recognized because you catch it, you need to not catch it or re-raise it. To re-raise just call raise without argument, inside a catch block, in your example:

 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

This will make you step out of that while, making it non-sense. I would move that while for another method, or function so that you can test IPv4Address raising behavior alone.

Another problem is calling input inside a function, is very annoying for testing. I would go for def validate_ip(ip):, much easier to test.

Regards,

geckos
  • 5,687
  • 1
  • 41
  • 53
0

The visible behavior on invalid input is that a new input is requested. The fact that it is done by means of catching an exception is an implementation detail that the tests would better not care about so that you can change that implementation detail without breaking the tests. The traceback in your question is what IPv4Address would do but not what validate_IP() would do.

Also you are expecting AddressValueError when the address is invalid, catching ValueError might hide other unexpected exceptions and make debugging harder so it you should better catch the more specific exception:

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.')

One story that you might want your tests to tell is this: if the input stream contains an invalid IP and then a valid one we discard the former and return the latter.

@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'))

BTW I suggest you rethink the names, I think validate_IP does not really says what the function is doing and the test names are pretty much meaningless.

Stop harming Monica
  • 12,141
  • 1
  • 36
  • 56