Lecture 8

Testing and Debugging

Burkay Genç, Ahmet Selman Bozkır, and Selma Dilek

26/04/2023

PREVIOUS LECTURE

  • dictionaries

TODAY

  • testing and debugging
    • how to make sure your code runs
    • how to check for existence of errors
    • how to track sources of errors

TESTING AND DEBUGGING

LIFE CAN BE UGLY

  • Life is never perfect
    • Internet fails to work
    • Phone gets disconnected
    • You team loses
    • Viruses kill people
  • We need to test things before using them
    • Check internet signal
    • Check phone signal
    • Training sessions
    • Examine virus in lab

What if something fails?

  • If something fails we need to fix it
    • But to fix something, you need to know why it was broken in the first place
  • In real life
    • Check internet cable, diagnose drivers, restart modem
    • Check airplane mode, move around the house, restart phone
    • Change tactics, change players, check equipment quality
    • Extract genome, test with animals, computer simulations

Strategy

Problem Fixing

WHAT IF CODE FAILS?

  • Code sometimes always fails
    • Even code written by experts can fail
    • It is not possible to foresee every possible scenario
  • You do not expect to write code in one run

DEFENSIVE PROGRAMMING

TESTING

SOME WISDOM

Program testing can be used to show the presence of bugs, but never to show their absence!

Edsger Dijkstra

No amount of experimentation can ever prove me right; a single experiment can prove me wrong.

Albert Einstein

^^^ One sentence summary of the “scientific method”

WHEN ARE YOU READY TO TEST?

  • ensure code runs
    • remove syntax errors
    • remove static semantic errors
    • Python interpreter can usually find these for you
  • have a set of expected results
    • an input set
    • for each input, the expected output

TESTING APPROACHES

  • intuition about natural boundaries to the problem
    • can you come up with some natural partitions?
def is_bigger(x, y):
  """ Assumes x and y are ints
  Returns True if y is less than x, else False """
  • if no natural partitions, might do random testing
    • probability that code is correct increases with more tests
    • better options below
  • black box testing
    • explore paths through specification
  • glass box testing
    • explore paths through code

BLACK BOX TESTING

def sqrt(x, eps):
  """ Assumes x, epsilon floats, x >= 0, epsilon > 0
  Returns res such that x-epsilon <= res*res <= x+epsilon """
  • designed without looking at the code
  • can be done by someone other than the implementer to avoid some implementer biases
  • testing can be reused if implementation changes
  • paths through specification
    • build test cases in different natural space partitions
    • also consider boundary conditions

BLACK BOX TESTING

def sqrt(x, epsilon):
  """ Assumes x, epsilon floats, x >= 0, epsilon > 0
  Returns res such that x-epsilon <= res*res <= x+epsilon """

GLASS BOX TESTING

  • use code directly to guide design of test cases
  • called path-complete if every potential path through code is tested at least once
  • what are some drawbacks of this type of testing?
    • can go through loops arbitrarily many times
    • missing paths
  • guidelines
    • branches
      • exercise all parts of a conditional
    • for loops
      • loop not entered
      • body of loop executed exactly once
      • body of loop executed more than once
    • while loops
      • same as for loops
      • cases that catch all ways to exit loop

EXAMPLE

def isPrime(x):
  """ Assumes x is a nonnegative int
  Returns True if x is prime; False otherwise """
  if x <= 2:
    return False
  for i in range(2, x):
    if x % i == 0:
      return False
  return True
  • What should we test for?

EXAMPLE

def isPrime(x):
  """ Assumes x is a nonnegative int
  Returns True if x is prime; False otherwise """
  if x <= 2:
    return False
  for i in range(2, x):
    if x % i == 0:
      return False
  return True

isPrime(0) # expected value: False
## False
isPrime(2) # expected value: True
## False
  • Oops! For x = 2, the answer should have been True!
  • We know we should test this because we could see the code.

GLASS BOX TESTING

def abs(x):
  """ Assumes x is an int
  Returns x if x>=0 and –x otherwise """
  if x < -1:
    return -x
  else:
    return x
  • a path-complete test suite could miss a bug
  • path-complete test suite: 2 and -2
  • but abs(-1) incorrectly returns -1
  • should still test boundary cases

GENERAL GUIDELINES

  • Exercise both branches of all if statements.
  • Make sure that each except clause (we will learn later) is executed.
  • For each for loop, have test cases in which
    • The loop is not entered (e.g., if the loop is iterating over the elements of a list, make sure that it is tested on the empty list),
    • The body of the loop is executed exactly once, and
    • The body of the loop is executed more than once.
  • For each while loop,
    • Look at the same kinds of cases as when dealing with for loops.
    • Include test cases corresponding to all possible ways of exiting the loop.
  • For recursive functions, include test cases that cause the function to return with
    • no recursive calls,
    • exactly one recursive call, and
    • more than one recursive call.

DEBUGGING

BUGS

  • When bugs enter electric equipment, they may break it
  • When virtual bugs enter software, they may break it
    • Except, virtual bugs cannot crawl into software
    • You put them there!
  • Debugging is the act of finding and removing bugs from software

Actual bug found in 1947 in Mark II Aiken Relay Calculator at Harvard University

DEBUGGING

  • steep learning curve
  • goal is to have a bug-free program
  • tools
    • built in to most IDEs
    • Python Tutor (excellent online tool for beginners)
    • print statement
    • use your brain, be systematic in your hunt

PRINT STATEMENTS

EXAMPLE

def average(li):
  """ Computes the average of numbers in a list """
  sum = li[0]
  for i in li:
    sum += i
  return (sum / len(li))
  
print(average([0,-1,1]))  ## Expect 0.0
## 0.0
print(average([0,1,2,3,4]))  ## Expect 2.0
## 2.0
print(average([1,2,3]))  ## Expect 2
## 2.3333333333333335
  • What is wrong?

EXAMPLE

  • Let’s add sum prints
def average(li):
  print("List input:", li)    ## Initial value of the argument
  """ Computes the average of numbers in a list """
  sum = li[0]
  for i in li:
    print("Adding:", i)   ## The number added in this iteration
    sum += i
    print("Current sum:", sum)    ## Value of sum after each loop iteration
  print("List length:", len(li))  ## The length of the list
  return (sum / len(li))
  
print(average([1,2,3]))  ## Expect 2
## List input: [1, 2, 3]
## Adding: 1
## Current sum: 2
## Adding: 2
## Current sum: 4
## Adding: 3
## Current sum: 7
## List length: 3
## 2.3333333333333335
  • How come when we add 1 the current sum becomes 2??

EXAMPLE

  • The suspect is this line: sum=li[0]
  • We are initializing sum with the first item
  • Then the first item is added again
  • So it is added twice into the sum
def average(li):
  print("List input:", li)    ## Initial value of the argument
  """ Computes the average of numbers in a list """
  sum = li[0]
  for i in li:
    print("Adding:", i)   ## The number added in this iteration
    sum += i
    print("Current sum:", sum)    ## Value of sum after each loop iteration
  print("List length:", len(li))  ## The length of the list
  return (sum / len(li))
  
print(average([1,2,3]))  ## Expect 2

EXAMPLE

  • How to fix?
    • Replace with sum = 0
def average(li):
  print("List input:", li)    ## Initial value of the argument
  """ Computes the average of numbers in a list """
  sum = 0     ## FIXED
  for i in li:
    print("Adding:", i)   ## The number added in this iteration
    sum += i
    print("Current sum:", sum)    ## Value of sum after each loop iteration
  print("List length:", len(li))  ## The length of the list
  return (sum / len(li))
  
print(average([1,2,3]))  ## Expect 2
## List input: [1, 2, 3]
## Adding: 1
## Current sum: 1
## Adding: 2
## Current sum: 3
## Adding: 3
## Current sum: 6
## List length: 3
## 2.0

EXAMPLE

def recursiveReverse(s):
  if len(s) <= 1:
    return ""
  else:
    return recursiveReverse(s[2:len(s)]) + s[0]

print("-", recursiveReverse("burkay"), "-")  # Expecting  "yakrub"
## - arb -
  • What????

EXAMPLE

  • Let’s add some prints
def recursiveReverse(s):
  print("Input string:", s)
  if len(s) <= 1:
    return ""
  else:
    print("Recursive call:", s[2:len(s)], "+", s[0])
    return recursiveReverse(s[2:len(s)]) + s[0]

print("-", recursiveReverse("burkay"), "-")  # Expecting  "yakrub"
## Input string: burkay
## Recursive call: rkay + b
## Input string: rkay
## Recursive call: ay + r
## Input string: ay
## Recursive call:  + a
## Input string: 
## - arb -
  • Problem 1: s[2:len(s)] is trimming 2 characters at once

EXAMPLE

  • Let’s fix problem 1
    • By replacing s[2:len(s)] with s[1:len(s)]
def recursiveReverse(s):
  print("Input string:", s)
  if len(s) <= 1:
    return ""
  else:
    print("Recursive call:", s[1:len(s)], "+", s[0])
    return recursiveReverse(s[1:len(s)]) + s[0]

print("-", recursiveReverse("burkay"), "-")  # Expecting  "yakrub"
## Input string: burkay
## Recursive call: urkay + b
## Input string: urkay
## Recursive call: rkay + u
## Input string: rkay
## Recursive call: kay + r
## Input string: kay
## Recursive call: ay + k
## Input string: ay
## Recursive call: y + a
## Input string: y
## - akrub -
  • Problem 2: the last character is lost!

EXAMPLE

  • Let’s fix problem 2
    • By replacing return "" with return s
def recursiveReverse(s):
  print("Input string:", s)
  if len(s) <= 1:
    return s
  else:
    print("Recursive call:", s[1:len(s)], "+", s[0])
    return recursiveReverse(s[1:len(s)]) + s[0]

print("-", recursiveReverse("burkay"), "-")  # Expecting  "yakrub"
## Input string: burkay
## Recursive call: urkay + b
## Input string: urkay
## Recursive call: rkay + u
## Input string: rkay
## Recursive call: kay + r
## Input string: kay
## Recursive call: ay + k
## Input string: ay
## Recursive call: y + a
## Input string: y
## - yakrub -
  • Fixed!!!

Exercises

  • Try to find some good test cases for the following code
    • It computes (3x2+2)/(2x21)
def foo(x):
    return (3*x**2+2)/(2*x**2-18)

Exercises

  • Try to find some good test cases for the following code
    • It computes (3x2+2)/(2x218)
def foo(x):
    return (3*x**2+2)/(2*x**2-18)
  • Did you realize that for x=3 and x=3 the code crashes.
foo(3)
## ZeroDivisionError: division by zero
  • How do you fix it?

Exercise

  • How do you fix it?
def foo(x):
    a = 3*x**2+2
    b = 2*x**2-18
    if b == 0:
        return None
    else:
        return a/b
  • Next week we will learn other (better) methods

Optional Exercise

  • Try to find the problem in the following code and fix it. It takes two lists and creates a list of pairs out of it:
def listMerge(l1, l2):
    l3 = []
    for i in range(len(l1)):
        l3.append( (l1[i], l2[i]) )
    return l3

listMerge([1,2,3], ['a', 'b', 'c'])
## [(1, 'a'), (2, 'b'), (3, 'c')]

Optional Exercise

  • Try to find the problem in the following code and fix it. It takes two circles and checks whether their regions intersect:
def circleIntersect(c1, c2):  # each circle is a triple (x,y,r)
    dist = ((c1[0]-c2[0])**2 + (c1[1]-c2[1])**2)**0.5
    if  dist >= c1[2] and dist < c1[2] + c2[2]:
        return True
    else:
        return False

circleIntersect((3,3,3),(-1,4,5))
## True

Optional Exercise

  • Try to find the problem in the following code and fix it. It takes two circles and checks whether their regions intersect:
def circleIntersect(c1, c2):  # each circle is a triple (x,y,r)
    dist = ((c1[0]-c2[0])**2 + (c1[1]-c2[1])**2)**0.5
    if  dist >= c1[2] and dist < c1[2] + c2[2]:
        return True
    else:
        return False
  • HINT: There are five potential cases. Test each.
    • No intersection
    • Outer tangential intersection
    • Partial intersection
    • Inner tangential intersection
    • Full intersection

Copyright Information