Extreme Teaching

Now that you're remembering back to your early days, perhaps you used to type in
-- I mean, perhaps you knew people who would type in -- long passages of a program and
then save and try to compile it. They spent a lot of time debugging the program. Maybe they put print statements here or there and commented out sections that weren't working, rather than use an actual debugger. In any case, they spent a lot of time trying to track down what usually amounted to little typos.

In extreme programming, you take very small steps. The goal is for the code to
always compile, and for all of the tests to pass. We might write a couple of lines of code
and then compile and run all of the tests. If something goes wrong, we have a good idea
where to look for our mistake. Here's a pseudo-code explanation of how we might proceed with the current program.

Running the Harness

First, before any tests are written, you should be able to run the test harness and
get some indication that it is up and running. In this case, there will be an executable
.jar file that you can run by double-clicking it. When it starts up, you will see
this:

Testing For the Existence of the Friend Class

Go ahead and click the button. Until the student has successfully created the shell for
a Friend class, they will see something like this:

Let's write a test to make sure we can create an object of type Friend. We'll do something like this:

This test shouldn't compile, because we haven't created a Friend class
yet. So we create a Friend class.

public class Friend(){
}

The code compiles and the test passes. Note that this exercise required that we do the following.

Use a text editor to create the code for the class Friend.

Save the code as Friend.java in a location in our classpath.

Compile Friend.java using javac or another compiler.

Run the test harness that checks the code.

This means that your two-line shell for the Friend class already serves as a system
check, just like the classic version of HelloWorld, without bogging you down
in a ton of details. You typed in Java code and you receive some sort of feedback
that your work was successful.

Testing For the Signature of the getName() Method

Once a Friend class exists in the correct location, the student will see
this feedback:

The next step is to write a test to make sure that the Friend class
contains a method, getName(), that returns a String that
is treated as the user's name. Our pseudo-code for the test might look like this:

Now you can see the benefit of working step by step. Once the first test passes,
we know that we can create an object of type Friend. Even
though the test seems trivial, we always run it as part of our test suite. If, down the
road, someone changes the source code and breaks this test, we'll immediately know
what went wrong and we'll know what needs fixing. These tests protect us against future
changes.

In this second test, we create a Friend object, invoke its getName() method and check that it returns a String. The instantiation
is repeated code from the first step. When we look at JUnit, we'll use its facility for
refactoring this common code to a single setUp() method. With this test method,
the student's code can't compile if the signature of getName() isn't correct. This is a bit
restrictive for a first assignment. In our actual harness code, you'll see that we test for a
correctly-named method and a correct return type separately, and give helpful feedback in
either case. The student can get the test to pass with code like this:

public class Friend {
public String getName(){
return "Any old String you want. " +
"It might be a name or it might be a meaningless sentence or two.";
}
}

Testing the Actual Value Returned

At this point, everything is syntactically correct in the student code. You may have noticed
so far that getting the code to compile led to the test being passed. We'll write one more test
that is a bit far-fetched in this example, but will serve to make a point. Suppose that I know
the student's name is Elena. I may want to test that the value that is returned by the getName() method matches "Elena." This test might look like this:

This code immediately compiles, because the class and the method exist and the method returns a variable
of the correct type. The test, however, will fail until the student changes the getName()
method to this:

public String getName(){
return "Elena";
}

Often, the purpose of unit tests is to test the return value of different methods in different situations.
In the current example, this is a bit silly, and so our test harness won't do it. Once the signature of getName() is correct, our harness will display whatever the student has used as the value being returned. They will see a screen like this:

Summary

In our revised HelloWorld assignment, the students have had to read existing code and craft an appropriate response. From the start, the return type of a method has meaning to them, because they are required to use it. They have been required to create from scratch a class and a method that conform to someone else's specifications. From the start, they understand that they may be only writing part of an application and that they have to be able to understand, conform to, and later, possibly contribute to the public interface.