T-Th 9:05
or
T-Th 11:15
in Olin 155

CS 1110: Introduction to Computing Using Python

Fall 2012

Functions and Testing

There is a PDF version of these instructions, if you would prefer to have that instead.

The purpose of this lab is to get you used to writing functions, and to introduce you to the basics of testing. As a warning, we will tell you right now: The module pointfuncs has errors in it; do not look for them and test them in the beginning. You should only correct the module when you are told. The point of this lab is get you in the habit of testing your programs. Adopting this testing habit will prove to be unbelievably useful, particularly for the first assignment.

Requirements For This Lab

We have created a few files for this lab. You should create a new directory on your hard drive and download the following modules into that directory:

The first two modules are the code that you will be testing. The module point provides a new type, called Point. We describe this type below, but you do not need to understand the contents of this file. The most important module is pointfuncs. This is the module that has mistakes, and you will (eventually) need to fix them.

The second two modules are used for testing. You will find that the module cunittest will be a valuable tool for you throughout the semester. This module contains the unit testing functions assert_equals and assert_true that we showed off in class. The second is a skeleton module (e.g. there is really nothing in it); it is where you will write your unit tests to find the errors in pointfuncs.

This lab will involve three different components. First, you will need to write answers to the questions that we pose below. Second, you will need to write a new module that is a unit test for pointfuncs. Finally, you will need to modify the contents of pointfuncs to fix the mistakes. When you are done, you should show all three to your lab instructor, who will record that you did it. You do not need to submit the paper with your answers, and you do not need to submit the computer files anywhere.

As always, if you do not finish during the lab, you have until the beginning of lab next week to finish it. You should always do your best to finish during lab hours. Remember that labs are graded on effort, not correctness.


The Module point

The module point provides a new type: Point. While this type is important for this lab, the contents of this module are not. In fact, you will not learn how to read a module like this until much later in the course. Instead, we describe this type here.

Objects of type Point are points in a 3-dimensional space. These objects have three attributes: x, y, and z. These attributes correspond to the 3 coordinates. Point objects do not have any methods that are relevant to this lab.

Like the Window type in the previous lab, you create Point objects with a constructor. The constructor Point takes three arguments to set the coordinates x, y, and z. For example, the constructor call

  Point(2,1,0)
creates a Point object (2,1,0) and returns the name of the object.


Creating a Unit Test

For the first part of the lab, you will create a unit test to test the functions of module pointfuncs. You will start by testing the procedure has_a_zero. This function should return true if at least one of the x, y, or z coordinates in the point p is 0. If none of them are zero, it returns false.

That is what the specification says, and but the function has a bug and does not work correctly. You are to test the program to find the bug. Some of you may see the error right away, but do not fix it. The purpose of this lab is to teach you testing. So we are going to take you through this process, step-by-step.


The Unit Test Module testfuncs

A unit test is a special module that is used to test other modules. We have provided you a file to get started — testfuncs.py — but it does not have any significant code in it yet. It just has the initial comments at the top of the module (and you should put your name in the right comment). It also has some import statements.

The import statements are important. Obviously you need to import pointfuncs, as that is what you are testing. We have used the from ... import notation so that you can call all of the functions without having to put pointfuncs in front of the function name. You also need to import point, as you will be using point objects in your testing.

The file also imports cunittest. This module provides the functions assert_equals and asser_true which you will use in unit testing. You will note that we have used the normal import keyword to import it, so all of those functions will need cunittest to work. We did this just to give you experience with both versions of import, not because it is necessary.

When you create future unit tests in this class (such as for the first assignment), they should start out very much like this skeleton. You need to import the module cunittest to use the testing functions, and you obviously need to import whatever module you are testing.


Add the Application Code

Unit tests are applications. An application is a Python module that executes when you type

  python module-name
from the command shell. You can also run an application by having its window as the active window in Komodo Edit and pressing the "run button".

An application ends in a very special bit of code that starts

if __name__ == "__main__":
The code that executes when you run the application is indented directly under this line, like in a function body. Right now, the application code should contain a single print statement, as follows:
if __name__ == "__main__":
    print "Module pointfuncs is working correctly"

When you are done, run the application. What happens?






Create the Test Procedure

You are going to create a procedure test_has_a_zero() which will test the procedure has_a_zero(p). Right now, this procedure should just be a "stub" (e.g. it should not do anything at all). To make a stub procedure, just put pass indented under the header. So right now, this procedure should look like this:

def test_has_a_zero():
    pass

Add a call to the procedure in the "application code" (e.g. the code indented under if __name__ ...). Add the call before the print statement. The idea is that, if anything goes wrong in this test procedure, the program will stop before printing out the final announcement.


Implement the First Test Case

In the body of function test_has_a_zero, write Python statements that do the following:
  • Create a Point object (0,0,0), using the constructor Point, and store the (name of the) object in a variable p.
  • Call has_a_zero(p) and put the result in a variable named result.
  • Call the procedure cunittest.assert_true(result).

If you want, you can combine the last two steps into a nested function call like

  cunittest.assert_true(has_a_zero(p))
where p is a variable that contains the (name of) the point object. The important thing here is that assert_true does nothing if the call has_a_zero(p) returns True, which it should because your point has all zeros. If anything is wrong, then the assert_true function will stop the entire program and notify you of the error.

You should run the unit test now. If you have done everything correctly, then the unit test should reach the message "Module pointfuncs is working correctly" If not, then you have actually made an error in the testing program. This can be frustrating, but it happens sometimes. One of the important challenges with debugging is understanding whether the error is in the tester or the testee.


Add More Test Cases for a Complete Test

Just because one test case worked does not mean that the function is correct. The function has_a_zero can be "true in more than one way". For example, it is true when x is 0, but none of the other coordinates are. Similarly it can be true when just y is 0, or when just z is zero.

We also need to test points that have no zeroes in them. It is possible that the bug in has_a_zero is that it returns True all the time. If it does not return False when the point has no zeroes, it is not working either.

Clearly, there are a lot of different points that we could test — effectively infinitely many. The idea is to pick test cases that are representative. Every possible input should be similar to, but not exactly the same, as one of the representative tests. For example, if we test one point with no zeroes, we are fairly confident that it works for all points with no zeroes. But testing (0,0,0) is not enough to test the other ways in which test_a_zero could be true.

How many representative test cases do you think that you need in order to make sure that the function is correct? Perhaps 6 or 7 or 8? Write down a list of test cases that you think will suffice to assure that the function is correct:





In test procedure test_has_a_zero(), Implement all these test cases in procedure test_has_a_zero(), using the assert_true function. The test procedure may have to create more than one instance of type Point in order to implement all of your test cases.


Test

Run the Python module testfuncs.py as an application. If an error message appears (so you do not get the final print statement), study the message and where the error occurred (you will be provided with a line number) to determine what is wrong. The error could be anywhere.

Fix and Repeat

You now have permission to fix the code in pointfuncs.py. However, you should restrict your fixes to the function has_a_zero(p) only, as this is the only thing that you are testing. Do not fix anything else yet.

Rerun the unit test as an application. Repeat this process (fix, then run) until there are no more error messages.


Test Procedure shift(p)

The function shift(p) is actually a procedure. It does not return anything. Instead, this procedure will change the contents of the object (e.g. the folder) whose name is in p. Read the specifications of this procedure to understand what it does. Testing this will be a little different from testing has_a_zero.

In module testfuncs.py, you should make up another test procedure, test_shift(), that will test the function shift(p). Make this procedure a stub for now. You should also add a call to this test procedure in the application code, before the final print statement.


Implement the First Test Case

This procedure should take a point, and "shift" all of the coordinates to the left (with the x coordinate moving to the z coordinate). To test this out, you need to add the following code to test_shift.

  • Create a Point object (0,0,1), using the constructor Point, and store the (name of the) object in a variable p.
  • Call the procedure shift(p).
  • Test that p is now the point (0,1,0).

The last step requires further details. You cannot write

  p == (0,1,0)
This will return False. That is because (0,1,0) is a value of a type that we have not yet seen in class (and will not see for a while). Instead, you have to check each of the attributes — x, y, and z — separately.

This time you are testing variables with int or float values, not just boolean. To test this type of value you need the function assert_equals. In assert_equals, you have a value that you expect which you compare against the value that you actually get. So to check that p is the point (0,1,0), you would add the following statements:

  cunittest.assert_equals(0,p.x)
  cunittest.assert_equals(1,p.y)
  cunittest.assert_equals(0,p.z)

Add these test cases to the test procedure test_shift and run the unit test as an application. There should not be an error this time; check your test procedure if you run into any problems.


Add More Test Cases for a Complete Test

Obviously, the point (0,1,0) is not enough to test this function; we told you there was an error, and you have not found an error yet. Why is this point not sufficient to test the function shift?





What are good points for testing out this function?





Implement these test case you chose, and run the unit test as an application. You should get an error message this time.


Isolate the Error

Unit tests are great at finding whether or not an error exists. They are not always great at telling you where the error occurred. The procedure shift has three lines of code. The error could have occurred at any one of them.

In programming, we often use print statements to help us isolate an error. Recall in the last lab that you were asked to write a sequence of assignment statements where you extracted a substring contained in quotes. If we ran into problems, we suggested that after every print statement you put the

  print var
where var is the name of the variable in the assignment statement just above it. This will help you "visualize" what is going on. Everytime a variable is created or changes value, it is important that the new value is what you expect it to be.

Open up pointfuncs and add these print statements to the function shift(p) (not test_shift()), one after each of the three assignment statements. Now run the unit test. Before you see the error message, you should see three numbers print out. Those are the result of your print statements.

To make them easier to understand, sometimes we like to add more information to the print statement, such as

  print "The variable p.x is "+`p.x`
If you want to make that change, fine. You should do whichever you are most comfortable with.


Fix and Test

You should now have enough information from these three print statements to see what the error is. Fix the error and test the procedure again by running the unit test.


Clean up shift(p)

Unlike unit tests, using print statements to isolate an error is quite invasive. You do not want those print statements showing information on the screen every time you run the procedure. So once you are sure the program is running correctly, you should remove them. You can either comment them out (fine in small doses, as long as it does not make your code unreadable), or you can delete them entirely.

However, once you remove these, it is important that you test the procedure one last time. You want to be sure that you did not accidentally delete the wrong line of code by accident.

Once you have removed all the print statements, and the unit test runs without errors, you are done with this procedure.


Test Function parse(s)

Read the specification for parse. This function takes a string like "(1,2,3)" and turns it into the equivalent point object. You should try to understand this function thoroughly, as it is very relevant to the first assignment.

Create a stub for a test procedure called test_parse() and add a call to it to the application code, just like you have done for the past two parts of the lab.


Implement the First Test Case

Testing this function is very similar to testing shift(p). The primary difference is that parse(s) is a function that returns a new point, not a procedure that modifies an existing point. So your test cases should be testing the point that parse(s) returns, not the string you pass to it. So your first test case should do the following.

  • Call parse("(1,2,3)") and assign the result to a variable p
  • Test that p is now the point (1,2,3).

Follow the steps from test_shift() for the second step.


Oops

Something bad happened. You did not get the nice error message from assert_equals this time. Python was not able to complete processing the function and gave you an error that looks like this:

File "pointfuncs.py", line 65, in parse
    p.y = float(ystring)
ValueError: could not convert string to float: 

In the previous examples, Python just gave you the wrong answer. This time it crashed (And you can tell it crashed because there is no "Quitting with Error").

Unit testing is not going to help you find an error like this. And the line number in the error message is no help either. That is just where Python found the error; the mistake could have been made earlier.

Once again, you need to isolate the error with print statements. After every single assignment statement, add a print statement displaying the value of the variable in the assignment statement above it. Run the unit test and look at what is displayed on the screen.

This should be enough information for you to find the error. The error here is a legitimate mistake that you might make in a function like this; we made it ourselves when we wrote this function, and then left it in for the lab. If you cannot find the error now, ask a consultant or instructor for help.


Fix and Test

Once you find the error, fix it. Run the test again, and fix it again if necessary. It is a good idea to leave the print statements in until you are sure that the function is correct. However, when it is correct, you should remove all of the print statements inside of parse (and test one last time!).


Add the Function first_inside_quotes(s)

You may notice that pointfuncs has a specification for a function called first_inside_quotes(s). If a string s contains at least two double quotes inside of it, this function returns the substring within the first pair of such quotes.

This might seem familiar. We asked you to do something like this on the last lab. The only difference is that now we are asking you to write it in function form. You will find this function incredibly useful for the first assignment.

Once you are done implementing the function, add one last test procedure: test_first_inside_quotes(). Make sure that your function is correct. When you are satisfied, you are done with the lab.