Testing Exercises

The following exercises do not contain solutions. Yet. Instead, we will be asking you to submit your solutions to these exercises and then we will post up solutions at the start of next week. We encourage you to discuss your approaches or solutions on the course forum!

Exercise 1: Mileage

The function 'convert_mileage' converts miles per gallon (US style) to liters per 100 km (metric style):


In [ ]:
gal_to_litre = 3.78541178
mile_to_km = 1.609344
    
def convert_mileage(mpg):
    '''Converts miles per gallon to liters per 100 km'''
    litres_per_100_km = 100 / mpg / mile_to_km * gal_to_litre
    return litres_per_100_km

Create a subdirectory in your version control directory called testing-exercises, then copy this function into a file in that directory called mileage.py. Add more code to that file to repeatedly ask the user for a mileage in miles per gallon, and output the mileage in liters per 100 km, until the user enters the string q. You will need to use the float() function to convert from string to a floating point number. Use the 'if __name__ == "__main__":' trick to ensure that the module can be imported without executing your testing code.

  1. Copy mileage.py to create tryexcept.py Add a try/except block to the new program to display a helpful message instead of crashing when users enter invalid input (such as the number "0" or the name of their favorite hockey team).

  2. Reading the function again, you realize that accepting 0 or negative values make no sense and should be reported as an error. Look at the exceptions defined in the exceptions module (see the online documentation or use the built-in help(...) or dir(...) functions) and decide which of Python's built-in exceptions is most appropriate to use for invalid input. Create a copy of 'tryexcept.py' called 'raiser.py' that raises this exception; modify the main body of your program to catch it; and add a comment inside the file explaining why you chose the exception you did. (Note: you have to call this file raiser.py, not raise.py because 'import raise' is an error. Can you see why?)

  3. According to Google, 20 miles per gallon are equivalent to 11.7607292 liters per 100 km. Use these values to write a unit test. Keep in mind that these floating values are subject to truncation and rounding errors. Save the test case in a file called test_mileage.py and run it using the nosetests command. Note: test_mileage.py should use 'from raiser import convert_mileage' to get the final version of your mileage conversion function.

  4. Now add a second test case, for 40 miles per gallon equivalent to 5.88036458 liters per 100 km and run the tests again. Unless you have already fixed the error that was present in the initial function, your test should fail. Find and fix the error; submit your new function in a file called 'final_mileage.py'.

Exercise 2: Testing Averages

The results of a set of experiments are stored in a file, where the i-th line stores the results of the i-th experiment as a comma-separated list of integers. A student is assigned the task of finding the experiment with the smallest average value. She writes the following code:


In [ ]:
def avg_line(line):
    values = line.split(',')
    count = 0
    total = 0
    for value in values:
        total += int(value)
        count += 1
    return total / count
    
def min_avg(file_name):
    contents = open(file_name)
    averages = []
    for (i, line) in enumerate(contents):
        averages.append((avg_line(line), i))
    contents.close()
    averages.sort()
    min_avg, experiment_number = averages[0]
    return experiment_number
  1. Refactor min_avg so that it can be tested without depending on external files. Submit your code in a file called first_averages.py.

  2. Write Nose test cases for both functions. Consider what should happen if the file is empty. Submit your tests in a file called test_first_averages.py. Note: you may assume for now that all input is well formatted, i.e., you do not have to worry about empty lines, lines containing the names of hockey teams, etc.

  3. The given specification is ambiguous: what should the result be if two or more experiments are tied for the minimum average? Copy 'first_averages.py' to create a new file 'second_averages.py'; modify it to handle this case; add a comment to the top explaining what rule you decided to use; and create a file 'test_second_averages.py' that tests your changes.

  4. Another student proposed an alternative implementation of the min_avg function:


In [ ]:
def min_avg(file_name):
    contents = open(file_name).readlines()
    min_avg = avg_line(contents[0])
    min_index = 0
    for (i,line) in enumerate(contents):
        current_avg = avg_line(line)
        if current_avg <= min_avg:
            min_avg = current_avg
            min_index = i
    return min_index

This implementation also finds an experiment with the smallest average, but possibly a different one than the your function. Modify your test cases so that both your implementation and this one will pass. (Hint: use the 'in' operator.)

  1. One way to avoid the ambiguity of this specification is to define a 'min_avg_all' function instead, which returns a list with all the experiments with the smallest average, and let the caller select one. Write tests for the 'min_avg_all' function, considering the following situations: an empty file, exactly one experiment with minimum average, and more than one experiment with minimum average. Keep in mind that in the last case, implementations could return the list in different order. Write the tests the file "test_averages.py". Use the same data as for the previous tests, if possible. You should use variables to avoid code duplication. You don't need to implement the 'min_avg_all' function, but your test cases should be comprehensive enough to serve as a specification for it.