Merge pull request #34 from pipitone/swc_exercises
[swc-testing-nose.git] / python / testing / exercises / test.markdown
1 The following exercises do not contain solutions. Yet. Instead, we will be
2 asking you to submit your solutions to these exercises and then we will post up
3 solutions at the start of next week. We encourage you to discuss your approaches
4 or solutions on the course forum!
5
6 To submit your exercises, please create a `testing` folder in your personal
7 folder in the course repository. Place all of the code and files for theses
8 exercises in that folder and be sure to check it in.
9
10
11 ## Exercise 1: Mileage
12
13 The function 'convert_mileage' converts miles per gallon (US style) to liters
14 per 100 km (metric style):
15
16 ```python
17     gal_to_litre = 3.78541178
18     mile_to_km = 1.609344
19     
20     def convert_mileage(mpg):
21         '''Converts miles per gallon to liters per 100 km'''
22         litres_per_100_km = 100 / mpg / mile_to_km * gal_to_litre
23         return litres_per_100_km
24 ```
25
26 Create a subdirectory in your version control directory called `testing`, then
27 copy this function into a file in that directory called `mileage.py`. Add more
28 code to that file to repeatedly ask the user for a mileage in miles per gallon,
29 and output the mileage in liters per 100 km, until the user enters the string
30 "`q`". You will need to use the `float()` function to convert from string to a
31 floating point number. Use the '`if __name__ == "__main__":`' trick to ensure
32 that the module can be imported without executing your testing code.
33
34 1. Copy `mileage.py` to create `tryexcept.py` Add a try/except block to the new
35 program to display a helpful message instead of crashing when users enter
36 invalid input (such as the number "0" or the name of their favorite hockey
37 team). 
38
39 2. Reading the function again, you realize that accepting 0 or negative values
40 make no sense and should be reported as an error. Look at the exceptions defined
41 in the `exceptions` module (use the built-in `help(...)` or `dir(...)`
42 functions) and decide which of Python's built-in exceptions is most appropriate
43 to use for invalid input. Create a copy of 'tryexcept.py' called 'raiser.py'
44 that raises this exception; modify the main body of your program to catch it;
45 and add a comment inside the file explaining why you chose the exception you
46 did. (Note: you have to call this file `raiser.py`, not `raise.py` because
47 'import raise' is an error. Â Can you see why?)
48
49 3. [According to
50 Google](http://www.google.ca/search?q=20+miles+per+gallon+in+litres+per+100+km&gbv=1),
51 20 miles per gallon are equivalent to 11.7607292 liters per 100 km. Use these
52 values to write a unit test. Keep in mind that these floating values are subject
53 to truncation and rounding errors. Save the test case in a file called
54 `test_mileage.py` and run it using the `nosetests` command. Â Note:
55 `test_mileage.py` should use '`from raiser import convert_mileage`' to get the
56 final version of your mileage conversion function.
57
58 4. Now add a second test case, for 40 miles per gallon equivalent to 5.88036458
59 liters per 100 km and run the tests again. Â Unless you have already fixed the
60 error that was present in the initial function, your test should fail. Â Find
61 and fix the error; submit your new function in a file called 'final_mileage.py'. 
62
63
64 ## Exercise 2: Testing Averages
65
66 The results of a set of experiments are stored in a file, where the _i-th_ line
67 stores the results of the _i-th_ experiment as a comma-separated list of
68 integers. A student is assigned the task of finding the experiment with the
69 smallest average value. She writes the following code: 
70
71 ```python
72     def avg_line(line):
73            values = line.split(',')
74            count = 0
75            total = 0
76            for value in values:
77                 total += int(value)
78                 count += 1
79            return total / count
80     
81        def min_avg(file_name):
82            contents = open(file_name)
83            averages = []
84            for (i, line) in enumerate(contents):
85                averages.append((avg_line(line), i))
86            contents.close()
87            averages.sort()
88            min_avg, experiment_number = averages[0]
89            return experiment_number
90 ```
91
92 1. Refactor `min_avg` so that it can be tested without depending on external
93 files. Submit your code in a file called `first_averages.py`.
94
95 2. Write Nose test cases for both functions. Consider what should happen if the
96 file is empty. Submit your tests in a file called `test_first_averages.py`.
97 Note: you may assume for now that all input is well formatted, i.e., you do
98 _not_ have to worry about empty lines, lines containing the names of hockey
99 teams, etc.
100
101 3. The given specification is ambiguous: what should the result be if two or
102 more experiments are tied for the minimum average?  Copy 'first_averages.py' to
103 create a new file 'second_averages.py'; modify it to handle this case; add a
104 comment to the top explaining what rule you decided to use; and create a file
105 'test_second_averages.py' that tests your changes.
106
107 4. Another student proposed an alternative implementation of the min_avg
108 function:
109
110 ```python
111     def min_avg(file_name):
112         contents = open(file_name).readlines()
113         min_avg = avg_line(contents[0])
114         min_index = 0
115         for (i,line) in enumerate(contents):
116             current_avg = avg_line(line)
117             if current_avg <= min_avg:
118                 min_avg = current_avg
119                 min_index = i
120         return min_index
121 ```    
122
123 This implementation also finds an experiment with the smallest average, but
124 possibly a different one than the your function.  Modify your test cases so that
125 both your implementation and this one will pass.  (Hint: use the 'in' operator.)
126
127 5. One way to avoid the ambiguity of this specification is to define a
128 'min_avg_all' function instead, which returns a list with all the experiments
129 with the smallest average, and let the caller select one. Write tests for the
130 'min_avg_all' function, considering the following situations: an empty file,
131 exactly one experiment with minimum average, and more than one experiment with
132 minimum average. Keep in mind that in the last case, implementations could
133 return the list in different order. Write the tests the file "test_averages.py".
134 Use the same data as for the previous tests, if possible. You should use
135 variables to avoid code duplication. You don't need to implement the
136 'min_avg_all' function, but your test cases should be comprehensive enough to
137 serve as a specification for it.