python-course.eu

13. Tests, DocTests, UnitTests

By Bernd Klein. Last modified: 08 Mar 2024.

Errors and Tests

Usually, programmers and program developers spend a great deal of their time with debugging and testing. It's hard to give exact percentages, because it highly depends on other factors like the individual programming style, the problems to be solved and of course on the qualification of a programmer. Without doubt, the programming language is another important factor.

You don't have to program to get pestered by errors, as even the ancient Romans knew. The philosopher Cicero coined more than 2000 years ago an unforgettable aphorism, which is often quoted: "errare humanum est"* This aphorism is often used as an excuse for failure. Even though it's hardly possible to completely eliminate all errors in a software product, we should always work ambitiously to this end, i.e. to keep the number of errors minimal.

To Err is Human

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

See our Python training courses

See our Machine Learning with Python training courses

Kinds of Errors

There are various kinds of errors. During program development there are lots of "small errors", mostly typos. Whether a colon is missing - for example, behind an "if" or an "else" - or the keyword "True" is wrongly written with a lower case "t" can make a big difference. These errors are called syntactical errors.** In most cases, syntactical errors can easily be found, but other type of errors are harder to solve. A semantic error is a syntactically correct code, but the program doesn't behave in the intended way. Imagine somebody wants to increment the value of a variable x by one, but instead of "x += 1" he or she writes "x = 1". The following longer code example may harbour another semantic error:

x = int(input("x? "))
y = int(input("y? "))
if x > 10:
    if y == x:
        print("Fine")
else:
    print("So what?")

OUTPUT:

So what?

We can see two if statements. One nested inside of the other. The code is definitely syntactically correct. But it can be the case that the writer of the program only wanted to output "So what?", if the value of the variable x is both greater than 10 and x is not equal to y. In this case, the code should look like this:

x = int(input("x? "))
y = int(input("y? "))
if x > 10:
    if y == x:
        print("Fine")
    else:
        print("So what?")

Both code versions are syntactically correct, but one of them violates the intended semantics. Let's look at another example:

for i in range(7):
     print(i)

OUTPUT:

0
1
2
3
4
5
6

The statement ran without raising an exception, so we know that it is syntactically correct. Though it is not possible to decide if the statement is semantically correct, as we don't know the problem. It may be that the programmer wanted to output the numbers from 1 to 7, i.e. 1,2,...7 In this case, he or she does not properly understand the range function.

So we can divide semantic errors into two categories.

Unit Tests

Taking the Temperature

This paragraph is about unit tests. As the name implies they are used for testing units or components of the code, typically, classes or functions. The underlying concept is to simplify the testing of large programming systems by testing "small" units. To accomplish this the parts of a program have to be isolated into independent testable "units". One can define "unit testing" as a method whereby individual units of source code are tested to determine if they meet the requirements, i.e. return the expected output for all possible - or defined - input data. A unit can be seen as the smallest testable part of a program, which are often functions or methods from classes. Testing one unit should be independent from the other units as a unit is "quite" small, i.e. manageable to ensure complete correctness. Usually, this is not possible for large scale systems like large software programs or operating systems.

Live Python training

instructor-led training course

Enjoying this page? We offer live Python training courses covering the content of this site.

Upcoming online Courses

See our Python training courses

See our Machine Learning with Python training courses

Module Tests with name

Every module has a name, which is defined in the built-in attribute __name__. Let's assume that we have written a module "xyz" which we have saved as "xyz.py". If we import this module with "import xyz", the string "xyz" will be assigned to __name__. If we call the file xyz.py as a standalone program, i.e. in the following way,

$python3 xyz.py

the value of __name__ will be the string '__main__'.

The following module can be used for calculating fibonacci numbers. But it is not important what the module is doing. We want to demonstrate, how it is possible to create a simple module test inside of a module file, - in our case the file "xyz.py", - by using an if statement and checking the value of __name__. We check if the module has been started standalone, in which case the value of __name__ will be __main__. Please save the following code as "fibonacci1.py":

-Fibonacci Module-

def fib(n):
    """ Calculates the n-th Fibonacci number iteratively """
    a, b = 0, 1
    for i in range(n):
        a, b = b, a + b
    return a
def fiblist(n):
    """ creates a list of Fibonacci numbers up to the n-th generation """
    fib = [0,1]
    for i in range(1,n):
        fib += [fib[-1]+fib[-2]]
    return fib

It's possible to test this module manually in the interactive Python shell:

from fibonacci1 import fib, fiblist
fib(0)

OUTPUT:

Test for the fib function was successful!
fib(1)

OUTPUT:

1
fib(10)

OUTPUT:

55
fiblist(10)

OUTPUT:

[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
fiblist(-8)

OUTPUT:

[0, 1]
fib(-1)

OUTPUT:

0
fib(0.5)

OUTPUT:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-d518fced90e3> in <module>
----> 1fib(0.5)
~/Dropbox (Bodenseo)/Bodenseo Team Folder/melisa/notebooks_en/fibonacci1.py in fib(n)
      2     """ Calculates the n-th Fibonacci number iteratively """
      3     a, b = 0, 1
----> 4for i in range(n):
      5         a, b = b, a + b
      6     return a
TypeError: 'float' object cannot be interpreted as an integer

We can see that the functions make only sense, if the input consists of positive integers. The function fib returns 0 for a negative input and fiblist returns always the list [0,1], if the input is a negative integer. Both functions raise a TypeError exception, because the range function is not defined for floats. We can test our module by checking the return values for some characteristic calls to fib() and fiblist(). So we can add the following if statement

if fib(0) == 0 and fib(10) == 55 and fib(50) == 12586269025:
    print("Test for the fib function was successful!")
else:
    print("The fib function is returning wrong values!")

to our module, but give it a new name fibonacci2.py. We can import this module now in a Python shell or inside of a Python program. If the program with the import gets executed, we receive the following output:

import fibonacci2

OUTPUT:

Test for the fib function was successful!

This approach has a crucial disadvantage. If we import the module, we will get output, saying the test was okay. This is omething we don't want to see, when we import the module. Apart from being disturbing it is not common practice. Modules should be silent when being imported, i.e. modules should not produce any output. We can prevent this from happening by using the special built-in variable __name__. We can guard the test code by putting it inside the following if statement:

if __name__ == "__main__":
    if fib(0) == 0 and fib(10) == 55 and fib(50) == 12586269025:
        print("Test for the fib function was successful!")
    else:
        print("The fib function is returning wrong values!")

The value of the variable __name__ is set automatically by Python. Let us imagine that we import some crazy module with the names foobar.py blubla.py and blimblam.py, the values of the variable __name__ will be foobar, blubla and blimblam correspondingly.

If we change our fibonacci module correspondingly and save it as fibonacci3.py, we get a silent import:

import fibonacci3

We were successful at silencing the output. Yet, our module should perform the test, if it is started standalone.

(base) bernd@moon:~/$ python fibonacci3.py 
Test for the fib function was successful!
(base) bernd@moon:~/$

If you want to start a Python program from inside of another Python program, you can do this by using the exec command, as we do in the following code:

exec(open("fibonacci3.py").read())

OUTPUT:

Test for the fib function was successful!

We will deliberately add an error into our code now.

We change the following line

 a, b = 0, 1 

into

 a, b = 1, 1 

and save as fibonacci4.py.

Principally, the function fib is still calculating the Fibonacci values, but fib(n) is returning the Fibonacci value for the argument "n+1". If we call our changed module, we receive this error message:

exec(open("fibonacci4.py").read())

OUTPUT:

The fib function is returning wrong values!

Let's rewrite our module:

""" Fibonacci Module """
def fib(n):
    """ Calculates the n-th Fibonacci number iteratively """
    a, b = 0, 1
    for i in range(n):
        a, b = b, a + b
    return a
def fiblist(n):
    """ creates a list of Fibonacci numbers up to the n-th generation """
    fib = [0,1]
    for i in range(1,n):
        fib += [fib[-1]+fib[-2]]
    return fib
if __name__ ==