Modules and Testing

Overview

Structuring Python
Programs

  • Imports
  • Modules
  • Packages

Testing Python
Programs

  • Why test?
  • Building Tests with Assert
  • Testing Packages

Modules

Structuring Python Programs

The Python import system can be used for:

  1. Standard Library Modules
    • Examples: sys, os, re, json, csv
  2. Installed Modules
    • Examples: pandas, requests, lxml
  3. Your Own Modules
    • Python files you create in the same directory

Import Examples

Import an entire module from the standard library:

import random
x = random.randint(1,100)       

Note the namespacing, “random.randint()”

Import Examples

Import one part of a module without namespacing:

from random import randint
x = randint(1,100)  

The randint() function is now in top-level scope.

Import Examples

Import a function or class under a different name:

from random import randint as rand
x = rand(1,100)

“rand” becomes an alias for random.randint()

Import Examples

Modules outside the standard library must be installed:

>>> import pandas
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'pandas'

Unless it is installed on your system, pandas is not available.

Import Examples

You can use pip/pip3 (Python Package Manager) to install non-standard modules:

$ pip3 install pandas
Collecting pandas
... lots of lines ...
Installing collected packages: ...
Successfully installed ... pandas-0.23.4 ...

Anyone who wants to run your code must similarly install pandas. This is known as a dependency.

Importing Your Own Modules

  • Imports are not just for other people’s code
  • As programs become complex, single-file programming gets impractical
  • To facilitate reuse and make code easier to understand, divide large programs into modules
  • Modules are just different .py files stored together

Importing Your Own Modules

Imagine you have a module called petshop.py that depends on pets.py, which includes two classes (Cat and Dog).

import pets
c = pets.Cat('Fluffy')
d = pets.Dog('Spot')

By importing “pets”, the classes in pets.py become available in petshop.py.

Python’s Search Path

During imports, Python searches the following:

  1. Directory where your program is located
  2. PYTHONPATH (environment variable)
  3. Standard library (located inside your copy of Python)
  4. Locations in a .pth file (a text file listing one directory per line)

Python Packages

  • Modules can be grouped
  • A group of modules in a directory is called a package
  • To be recognized as a package, you must include a file named __init__.py
  • To run a package, use the -m flag at runtime

Init Files

  • __init__.py allows Python to recognize the directory as a package
  • It can contain code but is not required to
  • Because it is run at load time, it is commonly used for initialization tasks

Testing

Image of computer log book with moth taped inside

What is Testing?

  • Not the same as debugging
  • There are many styles of testing (acceptance testing, load testing, penetration testing)
  • The style described here is “unit” testing
  • Unit testing means writing tests for discrete, small “units” of code (often individual functions or methods)

Why Test?

  • Bugs can be costly (testing helps catch them early)
  • Testing can save time on debugging
  • But testing itself also takes a lot of time
  • Testing is a form of risk management
  • Test-driven development (TDD)

Many Ways to Test

  • Using shell tools
  • Build your own tests using assert statements, try/except
  • In “if name equals main”
  • Using an external library:
    1. unittest
    2. pytest

Using Assert

  • Assert statements are a means of declaring expected state
  • Primary use case is for debugging
  • Declares an expectation, and raises “AssertionError” if expectation is False
  • Assert is designed to catch errors that would otherwise be missed
  • There is no need to test for things that would raise an exception anyway

Using Assert

Imagine a function designed to draw a triangle. You might create an assert statement to check that the interior angles add up to 180°:

assert sum(angles) == 180, 'angles must total 180'

assert len(angles) == 3, ‘must have 3 angles’

Examples (assert)

def mysum(a, b):
    return a + b

assert mysum(2,2) == 4
assert mysum(2,3) == 6  

The second (incorrect) assertion raises an error

Using “If Name Equals Main”

  • For modules to be imported, the main block can be used for tests
  • Works only for modules not designed to be run by themselves
  • Put tests in the “name equals main” block
  • These tests will be ignored when the module is imported
  • But they can conveniently be activated by running the module directly

Examples (Name Equals Main)

Imagine a function designed to evaluate whether or not a number is prime:

def is_prime(x):
    # body of function here ...

if __name__ == "__main__":
    assert is_prime(2) == True
    assert is_prime(7) == True
    assert is_prime(4) == False 

Testing with pytest

  • Works similarly to the use of asserts in if name equals main
  • Works with modules intended for import as well as those intended to be run directly
  • Define testing functions in the module named according to one of these patterns: ‘test_*.py’ or ‘*_test.py’

Testing with pytest (continued)

  • Will execute all such functions when run against a module or package
  • Prints a detailed report of the results to the console
  • Has advanced features, including integration with unittest

Examples (pytest)

def is_prime(x):
    if x == 2:
        return True
    else:
        for i in range(2,n):
            if x%i == 0:
                return False
        return True

def test_is_prime():
    assert is_prime(2) == True
    assert is_prime(7) == True
    assert is_prime(4) == False

Examples (pytest)

$ pytest check_prime.py
======== test session starts ========
platform darwin -- Python 3.7.0, pytest-3.8.2, 
    py-1.7.0, pluggy-0.7.1
rootdir: /Users/westgard/Desktop, inifile:
collected 1 item                                                                         

check_prime.py .   [100%]

======== 1 passed in 0.03 seconds ========