Python tests¶

  • Testing a module
  • Doctest

Testing a module¶

A common practice is to end any python module with a block of instructions conditioned by __name__ == '__main__', containing the module unitary tests.

In [1]:
%%writefile mypowers.py
"""My own set of power() and powers() functions."""
def powers(x:float, ps:list) -> list:
    """Raise a value to a collection of powers."""
    result = []
    for p in ps:
        result.append(x**p)
    return result

if __name__ == '__main__':
    res = powers(2, [2, 3, 4])
    print(res)
    if res!=[4, 8, 16]:
        print('PROBLEM !')
    else:
        print('OK')
Overwriting mypowers.py

If the file is executed as a main program, the condition __name__ == '__main__' will be true and the tests will be executed.

In [2]:
!python3 mypowers.py
[4, 8, 16]
OK

If the file is imported as a module, the variable __name__ will contains the name of the module, and the tests will not be executed.

In [3]:
import mypowers
print('Name:', mypowers.__name__)
print(mypowers.powers(2, [2, 3, 4]))
Name: mypowers
[4, 8, 16]

When the module becomes increasingly long and complex, it may becomes very tedious to setup all the tests and check all the expected results one by one.

Doctest¶

The doctest module scan docstrings for pieces of text that look like interactive Python sessions, and then executes those sessions to verify that they work exactly as shown.

In any docstring, one can insert some python lines, preceded by >>>. After each such line, the block of non empty lines is the expected output.

In [4]:
%%writefile mypowers.py
"""
My own set of power() and powers() functions.
>>> powers(2, [2, 3, 4])
[4,8,16]
"""
def powers(x:float, ps:list) -> list:
    """Raise a value to a collection of powers."""
    result = []
    for p in ps:
        result.append(x**p)
    return result

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

As usual, there is a final block conditioned by __name__ == "__main__", and one has to execute the file as a main program, so to trigger the tests.

In [5]:
!python3 mypowers.py
**********************************************************************
File "/builds/lalens/PyPlot/doc/source/_slides/notebooks/mypowers.py", line 3, in __main__
Failed example:
    powers(2, [2, 3, 4])
Expected:
    [4,8,16]
Got:
    [4, 8, 16]
**********************************************************************
1 items had failures:
   1 of   1 in __main__
***Test Failed*** 1 failures.

As you noticed, doctest blindly compares the actual and expected output, character after character. Let's correct.

In [6]:
%%writefile mypowers.py
"""
My own set of power() and powers() functions.
>>> powers(2, [2, 3, 4])
[4, 8, 16]
"""
def powers(x:float, ps:list) -> list:
    """Raise a value to a collection of powers."""
    result = []
    for p in ps:
        result.append(x**p)
    return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()
Overwriting mypowers.py
In [7]:
!python3 mypowers.py

If nothing happens, this simply means that all the tests run OK. If you want to check the details, use the -v option.

In [8]:
!python3 mypowers.py -v
Trying:
    powers(2, [2, 3, 4])
Expecting:
    [4, 8, 16]
ok
1 items had no tests:
    __main__.powers
1 items passed all tests:
   1 tests in __main__
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

Well, doctest is right : our test is not about the whole file, but precisely for the powers() function. Let's move the test.

In [9]:
%%writefile mypowers.py
"""My own set of power() and powers() functions."""

def powers(x:float, ps:list) -> list:
    """
    Raise a value to a collection of powers.
    >>> powers(2, [2, 3, 4])
    [4, 8, 16]
    """
    result = []
    for p in ps:
        result.append(x**p)
    return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()
Overwriting mypowers.py
In [10]:
!python3 mypowers.py -v
Trying:
    powers(2, [2, 3, 4])
Expecting:
    [4, 8, 16]
ok
1 items had no tests:
    __main__
1 items passed all tests:
   1 tests in __main__.powers
1 tests in 2 items.
1 passed and 0 failed.
Test passed.

Questions ?¶