10.4. Doctest

10.4.1. Running tests

10.4.1.1. Running tests with your IDE

  • View menu -> Run... -> Doctest in my_function

10.4.1.2. From code

if __name__ == "__main__":
    import doctest
    doctest.testmod()

10.4.1.3. From command line

Listing 84. Display only errors
python -m doctest example.py
Listing 85. With -v display progress
python -m doctest -v example.py

10.4.2. Test for bool return values

AGE_ADULT = 18

def is_adult(age):
    """
    Function checks if person is adult.
    Adult person is over 18 years old.

    >>> is_adult(18)
    True

    >>> is_adult(17.9)
    False
    """
    if age >= AGE_ADULT:
        return True
    else:
        return False

10.4.3. Test numeric return values

10.4.3.1. int values

def add_numbers(a, b):
    """
    >>> add_numbers(1, 2)
    3
    >>> add_numbers(-1, 1)
    0
    >>> add_numbers(0, 0)
    0
    """
    return a + b

10.4.3.2. float values

def add_numbers(a, b):
    """
    >>> add_numbers(2.5, 1.2)
    3.7

    >>> add_numbers(0.1, 0.2)
    0.30000000000000004

    >>> add_numbers(0.1, 0.2)
    0.1 + 0.2 == 0.3000...
    """
    return a + b

10.4.4. Test for str return values

10.4.4.1. Returning str

  • Python will change to single quotes in most cases

  • Python will change to double quotes to avoid escapes

Listing 86. Python will change to single quotes in most cases
def echo(text):
    """
    >>> echo('hello')
    'hello'

    # Python will change to single quotes in most cases
    >>> echo("hello")
    'hello'

    Following test will fail
    >>> echo('hello')
    "hello"

    Python will change to double quotes to avoid escapes
    >>> echo('It\\'s Twardowski\\'s Moon')
    "It's Twardowski's Moon"
    """
    return text

10.4.4.2. Testing print()

  • print() function results, don't have quotes

Listing 87. print() function results, don't have quotes
def echo(text):
    """
    >>> echo('hello')
    hello
    """
    print(text)
Listing 88. Testing print(str) with newlines
def echo(text):
    """
    >>> echo('hello')
    hello
    hello
    hello
    <BLANKLINE>
    """
    print(f'{text}\n' * 3)

10.4.5. Testing for exceptions

Listing 89. Testing for exceptions
def add_numbers(a, b):
    """
    >>> add_numbers('one', 'two')
    Traceback (most recent call last):
        ...
    TypeError: Argument must be int or float
    """
    if not isinstance(a, (int, float)):
        raise TypeError('Argument must be int or float')

    if not isinstance(b, (int, float)):
        raise TypeError('Argument must be int or float')

    return a + b

10.4.6. Using python statements in doctest

Listing 90. Using python statements in doctest
def when(date):
    """
    >>> from datetime import datetime, timezone
    >>> moon = datetime(1969, 7, 21, 17, 54, tzinfo=timezone.utc)
    >>> when(moon)
    1969-07-21 17:54 UTC
    """
    print(f'{date:%Y-%m-%d %H:%M %Z}')

10.4.7. Examples

10.4.7.1. Celsius to Kelvin temperature conversion

Listing 91. Celsius to Kelvin temperature conversion
def celsius_to_kelvin(temperature_in_celsius):
    """
    >>> celsius_to_kelvin(0)
    273.15

    >>> celsius_to_kelvin(1)
    274.15

    >>> celsius_to_kelvin(-1)
    272.15

    >>> celsius_to_kelvin(-273.15)
    0.0

    >>> celsius_to_kelvin(-274.15)
    Traceback (most recent call last):
        ...
    ValueError: Argument must be greater than -273.15

    >>> celsius_to_kelvin([-1, 0, 1])
    Traceback (most recent call last):
        ...
    ValueError: Argument must be int or float

    >>> celsius_to_kelvin('one')
    Traceback (most recent call last):
        ...
    ValueError: Argument must be int or float
    """
    if not isinstance(temperature_in_celsius, (float, int)):
        raise ValueError('Argument must be int or float')

    if temperature_in_celsius < -273.15:
        raise ValueError('Argument must be greater than -273.15')

    return float(temperature_in_celsius + 273.15)

10.4.8. Assignments

10.4.8.1. Refactoring

  • Complexity level: easy

  • Lines of code to write: 5 lines of code

  • Estimated time of completion: 15 min

  • Filename: solution/doctest_temp.py

English
  1. Write implementation of a function from input code (see below)

  2. All tests must pass

Polish
  1. Napisz implementację funkcji z kodu wejściowego (patrz poniżej)

  2. Wszystkie testy muszą przechodzić

Input Code
def celsius_to_kelvin(degrees):
    """
    >>> celsius_to_kelvin(0)
    273.15
    >>> celsius_to_kelvin(1)
    274.15
    >>> celsius_to_kelvin(-1)
    272.15
    >>> celsius_to_kelvin('a')
    Traceback (most recent call last):
        ...
    TypeError: Invalid argument
    >>> celsius_to_kelvin([0, 1])
    [273.15, 274.15]
    >>> celsius_to_kelvin((0, 1))
    (273.15, 274.15)
    >>> celsius_to_kelvin({0, 1})
    {273.15, 274.15}
    """
    return ...

10.4.8.2. Distance conversion doctest

  • Complexity level: easy

  • Lines of code to write: 5 lines of code + 16 lines of tests

  • Estimated time of completion: 10 min

  • Filename: solution/doctest_distance.py

English
  1. Write functions which convert distance given in kilometers to meters

  2. 1 km = 1000 m

  3. Distance cannot be negative

  4. Returned value must by float

  5. Write doctests

Polish
  1. Napisz funkcję, która przeliczy dystans podany w kilometrach na metry

  2. 1 km = 1000 m

  3. Dystans nie może być ujemny

  4. Zwracany dystans musi być float

  5. Napisz doctesty

Output
  • Test arguments:

    • -1

    • 0

    • 1

    • float

    • int

    • str -> expect TypeError

    • any other type -> expect TypeError

    • True

10.4.8.3. Fix URL Regex

  • Complexity level: hard

  • Lines of code to write: 0 lines (discussion only)

  • Estimated time of completion: 5 min

English
  1. Copy code input from listing below

  2. Pattern incorrectly classifies https://foo_bar.example.com/ as invalid

  3. Fix pattern without automated tests

  4. Don't break classification of the other cases

Polish
  1. Skopiuj kod z listingu poniżej

  2. Wyrażenie niepoprawnie klasyfikuje https://foo_bar.example.com/ jako nieprawidłowy

  3. Popraw wyrażenie bez testów automatycznych

  4. Nie zepsuj klasyfikacji pozostałych przypadków

Input
# @diegoperini --  https://mathiasbynens.be/demo/url-regex
PATTERN = r'_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)[email protected])?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iuS'

VALID = [
    'http://foo.com/blah_blah',
    'http://foo.com/blah_blah/',
    'http://foo.com/blah_blah_(wikipedia)',
    'http://foo.com/blah_blah_(wikipedia)_(again)',
    'http://www.example.com/wpstyle/?p=364',
    'https://www.example.com/foo/?bar=baz&inga=42&quux',
    'http://✪df.ws/123',
    'http://userid:[email protected]:8080',
    'http://userid:[email protected]:8080/',
    'http://[email protected]',
    'http://[email protected]/',
    'http://[email protected]:8080',
    'http://[email protected]:8080/',
    'http://userid:[email protected]',
    'http://userid:[email protected]/',
    'http://142.42.1.1/',
    'http://142.42.1.1:8080/',
    'http://➡.ws/䨹',
    'http://⌘.ws',
    'http://⌘.ws/',
    'http://foo.com/blah_(wikipedia)#cite-1',
    'http://foo.com/blah_(wikipedia)_blah#cite-1',
    'http://foo.com/unicode_(✪)_in_parens',
    'http://foo.com/(something)?after=parens',
    'http://☺.damowmow.com/',
    'http://code.google.com/events/#&product=browser',
    'http://j.mp',
    'ftp://foo.bar/baz',
    'http://foo.bar/?q=Test%20URL-encoded%20stuff',
    'http://مثال.إختبار',
    'http://例子.测试',
    'http://उदाहरण.परीक्षा',
    'http://-.~_!$&\'()*+,;=:%40:80%2f::::::@example.com',
    'http://1337.net',
    'http://a.b-c.de',
    'http://223.255.255.254',
    'https://foo_bar.example.com/',
]

INVALID = [
    'http://',
    'http://.',
    'http://..',
    'http://../',
    'http://?',
    'http://??',
    'http://??/',
    'http://#',
    'http://##',
    'http://##/',
    'http://foo.bar?q=Spaces',
    '//',
    '//a',
    '///a',
    '///',
    'http:///a',
    'foo.com',
    'rdar://1234',
    'h://test',
    'http:// shouldfail.com',
    ':// should fail',
    'http://foo.bar/foo(bar)baz quux',
    'ftps://foo.bar/',
    'http://-error-.invalid/',
    'http://a.b--c.de/',
    'http://-a.b.co',
    'http://a.b-.co',
    'http://0.0.0.0',
    'http://10.1.1.0',
    'http://10.1.1.255',
    'http://224.1.1.1',
    'http://1.1.1.1.1',
    'http://123.123.123',
    'http://3628126748',
    'http://.www.foo.bar/',
    'http://www.foo.bar./',
    'http://.www.foo.bar./',
    'http://10.1.1.1',
    'http://10.1.1.254',
]