Python
What are those three lines between def a(x): and return x + 1? These lines are called the docstring of the function. A docstring is a special type of comment that is used to document what your function is doing. Typically, docstrings will explain what the function expects the type(s) of the argument(s) to be, and what the function is returning. In Python, docstrings appear immediately after the def line of a function, before the body. Docstrings start and end with triple quotes - this can be triple single quotes or triple double quotes, it doesn’t matter as long as they match. To sum up this general form:
Week 4: Good Programming Practices > 7. Testing and Debugging
Black-box testing is a method of software testing that tests the functionality of an application. Recall from the lecture that a way to think about black-box testing is to look at both:
The possible paths through the specification.
The possible boundary cases.
Undoubtably many - if not all - of the listed tests look like they would be pretty good for testing the function size. However, we want you to think critically about the way size is specified - including possible boundary cases - and pick a set of tests that adequately and fully tests all paths and boundary conditions. Be sure the set of tests you pick does not have extraneous, useless, or repetitive tests.
A good black box test suite would contain tests for the following conditions: Empty set, Set of size 1, and Set of size greater than 1.
Black-box testing is a method of software testing that tests the functionality of an application. Recall from the lecture that a way to think about black-box testing is to look at both the paths through the specification and the possible boundary cases. In this example, the boundary cases all have to do with the size of aSet. Specifically these boundary cases are when aSet contains zero, one, or many items.
The remaining conditions would not further test the functionality of the size function because an odd, even, or prime sized set are all sets of size greater than 1. Nothing in the function specification suggests there is anything special or unique about odd, even, or prime sized sets, so testing those cases specifically simply repeats the test “Set of size greater than 1”.
a path-complete glass box test suite would find test cases that go through every possible path in the code. This is different from black-box testing, because in black-box testing you only have the function specification. For glass-box testing, you actually know how the function you are testing is defined. Thus you can use this definition to figure out how many different paths through the code exist, and then pick a test suite based on that knowledge.
Undoubtably many - if not all - of the listed tests look like they would be pretty good for testing the function maxOfThree. However, we want you to think critically about the way maxOfThree is defined - including possible boundary cases - and pick a set of tests that adequetly and fully tests all paths and boundary conditions. A good first step will be to look at the function definition and figure out what paths through the code exist. Then, look through the suggested test suites one test at a time and see if the suite tests every single path.
def maxOfThree(a,b,c) :
"""
a, b, and c are numbers
returns: the maximum of a, b, and c
"""
if a > b:
bigger = a
else:
bigger = b
if c > bigger:
bigger = c
return bigger
a path-complete glass box test suite would find test cases that go through every possible path in the code. In this case, that means finding all possibilities for the conditional tests a > b and c > bigger. So, we end up with four possible paths that correspond to Test Suite A:
Case 1: a > b and c > bigger - this corresponds to test maxOfThree(2, -10, 100).
Case 2: a > b and c <= bigger - this corresponds to test maxOfThree(6, 1, 5)
Case 3: a <= b and c > bigger - this corresponds to test maxOfThree(7, 9, 10).
Case 4: a <= b and c <= bigger - this corresponds to test maxOfThree(0, 40, 20)
Test Suite B seems to explicitly test each of a, b, and c being the max of the three numbers, but this does not go through all possible paths in the code.
Test Suite C seems to be a good sampling of the space of input numbers, but it does not go through all possible paths in the code.
def union(set1, set2):
"""
set1 and set2 are collections of objects, each of which might be empty.
Each set has no duplicates within itself, but there may be objects that
are in both sets. Objects are assumed to be of the same type.
This function returns one set containing all elements from
both input sets, but with no duplicates.
"""
if len(set1) == 0:
return set2
elif set1[0] in set2:
return union(set1[1:], set2)
else:
return set1[0] + union(set1[1:], set2)
A good glass box test suite would try to test a good sample of all the possible paths through the code. So, it should contain tests that test when set1 is empty, when set1[0] is in set2, and when set1[0] is not in set2. The test suite should also test when the recursion depth is 0, 1, and greater than 1.
Remember that glass box testing is a method of software testing that tests the internal structures and workings of a piece of code. When we look at the code for union, we see a set of conditionals that ask about set1. Thus a good glass box test suite will contain tests that match the following lines from the conditional statements in the code:
len(set1) == 0 - matched by the test union('', 'abc')
set1[0] in set2 - matched by the test union('a', 'abc')
set1[0] not in set2 (this is the else: of the conditional) - matched by the test union('d', 'abc')
In addition, because union is a recursive function, we should make sure our test set considers a recursion depth of 0, 1, and many. In this case, recursion depth is covered by some of the tests we’ve already chosen:
Recursion depth = 0 - matched by the test union('', 'abc')
Recursion depth = 1 - matched by the tests union('a', 'abc'), union('d', 'abc')
Recursion depth > 1 - matched by the test union('ab', 'abc')
Note that this test suite is NOT path complete because it would take essentially infinite time to test all possible recursive depths.
Let’s examine now why the other test suites weren’t as good as Test Suite D:
Test Suite A looks at a good sampling of set sizes for set1 and set2, but does not explore the if-else paths in the code. set1 never contains any element in set2.
Test Suite B does not explore the paths in the code because it never varies the contents of set1.
Test Suite C does not contain a test that explores the path when set1 has an element that is not in set2.
In glass box testing, we try to sample as many paths through the code as we can. In the case of loops, we want to sample three general cases:
Not executing the loop at all.
Executing the loop exactly once.
Executing the loop multiple times.
Test Suite B has cases that explores all three loop cases.
Test Suite A only explores paths that execute the loop 0 or 1 times.
Test Suite C only explores paths that execute the loop more than 1 time.
#
raise Exception(“0”) just lets you raise your own generic exception titled “0”. If you ever did this in real code it would probably be something like “Exception(“Invalid input”) or something more helpful.
The except Exception as ex: just gives you a name for the exception, so you can print it out on the next line:
try: raise Exception(“Test exception”) except Exception as ex: print(ex)
When the above code runs, it will raise the generic exception with a title “Test exception”, then in the except block, it will catch that exception and print out the title:
Output:
Test exception
Using something like “except Exception as ex” just gives you a name (ex) for the exception that you can then use to print it out, or log it somewhere.
def fancy_divide(list_of_numbers, index): try: try: denom = list_of_numbers[index] for i in range(len(list_of_numbers)): list_of_numbers[i] /= denom finally: raise Exception(“0”) except Exception as ex: print(ex)
Since it is just the general Exception type, and not a specific one like ValueError, it will catch most types of exceptions. So it’s useful to give it a name (ex), so you can print it out for example and figure out what the problem is. BTW you can call it anything you want, “ex” is just a convention.
Just so you know, these are pretty convoluted examples, and not really things you see in real code. They are just to challenge you and make sure you understand the mechanics. You would probably never raise your own exception in the finally block, and it’s generally bad practice to catch the general Exception, as you should catch specific exceptions like ValueError or ZeroDivisionError based on what you are expecting. Because likely the code to handle those two cases would be different. except blocks are for fixing the problem usually, not just printing out an error message like we often do here.
Here is a more useful example, that tries to get a valid integer from the user, and catches exceptions if they enter some other data:
while True: user_input = input(“Please enter a number: “) try: user_int = int(user_input) except ValueError as ex: print(“That was not an integer, try again”) print(ex) else: break # else block is executed if there was no exception # which means the user properly entered a number, so # we can break the loop
loop is over, so user_int is now valid:
print(“Valid int received:”, user_int)
So that code will keep looping forever, asking the user for input, and trying to turn it into an int. If they enter a letter, or a string, or something like that, it will raise a ValueError exception, which we catch and give them a warning. We also print out the exception. Once they enter a proper integer, the else block is executed, and the break exits the loop.
As Kiwi said, it’s always helpful to play around with it yourself. You can test this code here:
https://repl.it/DocA/1