Software Carpentry logo

Quality Assurance

April 24, 2010: We are pleased to announce that Version 4 of this course is now under development. For updates and an early peek at the content, please check out the Software Carpentry blog at http://www.software-carpentry.org/blog/.

1) Introduction

2) You Can Skip This Lecture If...

3) Limits to Testing

4) Terminology

5) Test Results and Specifications

6) Structuring Tests

7) A Simple Example

Tests = [
    # String  Prefix  Expected
    ['a',     'a',    True],
    ['a',     'b',    False],
    ['abc',   'a',    True],
    ['abc',   'ab',   True],
    ['abc',   'abc',  True],
    ['abc',   'abcd', False],
    ['abc',   '',     True]
]
passes = 0
failures = 0
for (s, p, expected) in Tests:
    actual = s.startswith(p)
    if actual == expected:
        passes += 1
    else:
        failures += 1
print 'passed', passes, 'out of', passes+failures, 'tests'

8) Catching Errors

9) Simple Exception Example

for num in [-1, 0, 1]:
    try:
        inverse = 1/num
    except:
        print 'inverting', num, 'caused error'
    else:
        print 'inverse of', num, 'is', inverse
inverse of -1 is -1
inverting 0 caused error
inverse of 1 is 1
Flow of Control in Try/Except/Else

Figure 15.1: Flow of Control in Try/Except/Else

10) Exception Objects

values = [0, 1, 'momentum']
for i in range(4):
    try:
        print 'dividing by value', i
        x = 1.0 / values[i]
        print 'result is', x
    except ZeroDivisionError, e:
        print 'divide by zero:', e
    except IndexError, e:
        print 'index error:', e
    except:
        print 'some other error:', e
dividing by value 0
divide by zero: float division
dividing by value 1
result is 1.0
dividing by value 2
some other error: float division
dividing by value 3
index error: list index out of range

11) Exception Hierarchy

Name Purpose
Exception Root of exception hierarchy
ArithmeticError Illegal arithmetic operation
FloatingPointError Generic error in floating point calculation
OverflowError Result too large to represent
ZeroDivisionError Attempt to divide by zero
IndexError Bad index to sequence (out of bounds or illegal type)
TypeError Illegal type (e.g., trying to add integer and string)
ValueError Illegal value (e.g., math.sqrt(-1))
EnvironmentError Error interacting with the outside world
IOError Unable to create or open file, read data, etc.
OSError No permissions, no such device, etc.

Table 15.1: Common Exception Types in Python

12) Functions and Exceptions

Stacking Exception Handlers

Figure 15.2: Stacking Exception Handlers

def invert(vals, index):
    try:
        vals[index] = 10.0/vals[index]
    except ArithmeticError, e:
        print 'inner exception handler:', e

def each(vals, indices):
    try:
        for i in indices:
            invert(vals, i)
    except IndexError, e:
        print 'outer exception handler:', e

# Once again, the top index will be out of bounds.
values = [-1, 0, 1]
print 'values before:', values
each(values, range(4))
print 'values after:', values
values before: [-1, 0, 1]
inner exception handler: float division
outer exception handler: list index out of range
values after: [-10.0, 0, 10.0]

13) Raising Exceptions

for i in range(4):
    try:
        if (i % 2) == 1:
            raise ValueError('index is odd')
        else:
            print 'not raising exception for %d' % i
    except ValueError, e:
        print 'caught exception for %d' % i, e
not raising exception for 0
caught exception for 1 index is odd
not raising exception for 2
caught exception for 3 index is odd

14) Exceptional Style

15) Handling Errors in Tests

Tests = [
    ['a',     'a',    False],    # wrong expected value
    ['a',     1,      False],    # wrong type
    ['abc',   'a',    True]      # everything legal
]

passes = failures = errors = 0
for (s, p, expected) in Tests:
    try:
        actual = s.startswith(p)
        if actual == expected:
            passes += 1
        else:
            failures += 1
    except:
        errors += 1

print 'tests:', passes + failures + errors
print 'passes:', passes
print 'failures:', failures
print 'errors:', errors
tests: 3
passes: 1
failures: 1
errors: 1

16) Test-Driven Design

17) TDD Example

Tests = [
    [[],        [],          'empty list'],
    [[1],       [1],         'single value'],
    [[1, 3],    [1, 4],      'two values'],
    [[1, 3, 7], [1, 4, 11],  'three values'],
    [[-1, 1],   [-1, 0],     'negative values'],
    [[1, 3.0],  [1, 4.0],    'mixed types'],
    ["string",  ValueError,  'non-list input'],
    [['a'],     ValueError,  'non-numeric value']
]

18) Design by Contract

19) Assertions

def find_range(values):
    '''Find the non-empty range of values in the input sequence.'''
    assert (type(values) is list) and (len(values) > 0)
    left = min(values)
    right = max(values)
    assert (left in values) and (right in values) and (left <= right)
    return left, right

20) Defensive Programming

21) It's Never Too Late to Do It Right

def can_transmute(element):
    '''Can this element be turned into gold?'''

    # Bug #172: make sure the input is actually an element.
    assert is_valid_element(element)

    # Gold is trivial.
    if element is Gold:
        return True

    # Trans-uranic metals and halogens are impossible.
    if (element.atomic_number > Uranium.atomic_number) or \
       (element in Halogens):
        return False

    # Look for a sequence of steps that leads to gold.
    steps = search_transmutations(element, Gold)
    if steps == []:
        return False
    else:
        # Bug #201: must be at least two elements in sequence.
        assert len(steps) >= 2
        return True

22) Summary