Python Testing Design and Clarity Guide

Testing

  1. All functions require examples/test cases, including helper functions and testing functions
  2. Examples/test cases should reflect various input scenarios so that you exercise the possible main behaviors of your function. You do not need to test every input or every input combination, but rather enough to be confident that your function works with various inputs.
  3. Test the “edge cases” of functions. For example, functions that take in numeric inputs and work differently depending on the range that number is in should test the boundaries of those ranges.
  4. Even if your function does not work as expected, you can still write examples/test cases for it and receive full testing credit.
  5. If a function has a side effect (like modifying a list), test that side effect.
  6. If a function both has a side effect and returns information, test both the main return and the side effect.
  7. If a function fails in certain cases, test that failure using testValueError.
  8. If a function modifies (or mutates) data, set up testing data rather than working on a global piece of data/dataset.
  9. Use the testlight.py file to write test cases.

Setting up testlight

from testlight import *

def rem_if_gt_3(lst : list, numb : int):
    """ given a list and a number, remove that
        number from the list if it is greater than 3
        do nothing if the numb is <= 3. raise an error
        if numb is not in the list """
    if numb not in lst:
        raise ValueError('numb must be in list')
    if numb > 3:
        lst.remove(numb)

def test_rem():
""" tests the rem_if_gt_3 function """
    test_list = [0, 4, 5, 3]
    rem_if_gt_3(test_list, 4)
    test("test rem succ", test_list, [0, 5, 3])
    test("test rem None", rem_if_gt_3(test_list, 0), None)
    testValueError("test rem fail (> 3)",
                   lambda: rem_if_gt_3(test_list, 6))
    testValueError("test rem fail (<= 3)",
                   lambda: rem_if_gt_3(test_list, 2))
    

Design

  1. Use constants where appropriate.
  2. Use helper functions where appropriate.
  3. Make sure your helper functions and constants are not redundant.
    There is no point in making this function:
    def string_to_lower(s : str) -> str:
        return s.lower()
    
    since you could always use s.lower() instead.

Clarity

  1. Write docstrings for all functions, including helper and nested functions.
    A good docstring gives a description of the function, including its input(s) and output. Ideally, by looking at the docstring you know what the function does and how to use it without looking at the function body itself.

    You can include docstrings as a comment with """ just below your function signatures. E.g.

        def sum_of_list(l : list) -> int:
            """Computes the sum of integers in a list"""
    
  2. Give constants and helper functions useful names.

  3. All functions require type annotations on inputs and output. You can omit output annotation for functions that have no return (for example functions that only need to print).

  4. Names of constants and functions should be lower case and underscore separated. For configuration constants (for example the height of a character) it is acceptable to use all caps names.

  5. Keep lines under 80 characters.

  6. Indent your code properly. If you are using Pycharm, this should be happening automatically. If you are not using Pycharm, use 4 spaces to indent your code (not the tab character) and keep arguments of functions lined up:

def func_name(long_parameter_1 : int, long_parameter_2 : str
              long_parameter_3 : list, long_parameter_4 : list) -> int:
    ...
    
func_name(long_argument_1, long_argument_2,
          long_argument_3, long_argument_4)
  1. return statements should be not be written like this:
return(value)

but rather like this:

return value
  1. if statements should be written with newlines:
if condition1:
    print('condition1')
elif condition2:
    print('condition2')
else:
    print('condition3')