Test Infected Programming with Karel J. Robot

A recently developed program development methodology called Extreme Programming (XP) has had a big impact on the professional world and is starting to have one on education as well. It has twelve main practices, two of which are Pair Programming (programmers always work in pairs, never alone) and Test First (never write or modify code unless you first have a test for it.)

The latter practice is sometimes called Test Infected Programming.

Test first programming is made easier with a special tool that will let you easily collect all your tests into a test framework and run them simply -- for example by clicking a button. When your code changes or you write a new test, the framework knows about the changes. These tools exist for many languages. For Java, a good one is called JUnit that is available (free) from http://junit.org or from http://junit.sourceforge.net. Look at junit.org for Getting Started and for the JUnit Cookbook.

There is now a new test harness for Karel J. Robot that you can use to do test infected programming with robots. It is included in the regular distribution. To use it, however, requires that you have JUnit installed on your system and included in your project. It is just another jar file, junit.jar, like the KarelJRobot.jar.

What follows is a virtual transcript of two students, Lisa and Tony, working together at one computer to develop a StairClimber robot similar to the one in Chapter three of the book. At any given time, one of the pair is "driving" and has control of the keyboard and mouse, and the other is "navigating" and is making suggestions and watching for errors. The two roles pass back and forth among the students. Lisa is initially driving.


Lisa: If I understand the problem, we have to start at the origin, climb three stairs and then pick up a single beeper. Right?

Tony: Right. We start facing East. Oh, and the robot has to turnOff at the end.

Lisa: Right. I guess we should start by setting up the test framework for it. I think we can copy and rename the skeleton. Let's call the class we want to build StairClimber. OK.

Tony: OK. And so the test class should probably be StairClimberTest.

Lisa: OK. Here we go. (opens a template file renames the class (three places) and saves as StairClimberTest.java...)

package kareltherobot;

public class StairClimberTest extends KJRTest implements Directions
{
	public StairClimberTest(String name)
	{ 	super(name);
	}
		
	public static void main(String[] args)
	{
		junit.swingui.TestRunner.run(StairClimberTest.class);
	}
}

Lisa: There. Now we want a new StairClimber robot to work with and we need to initialize it in setUp. (typing...)

package kareltherobot;

public class StairClimberTest extends KJRTest implements Directions
{
	public StairClimberTest(String name)
	{ 	super(name);
	}

	private StairClimber karel;

	public void setUp()
	{	karel= new StairClimber(1, 1, East, 0);
	}


	public static void main(String[] args)
 	{
		junit.swingui.TestRunner.run(StairClimberTest.class);
	}
}

How is that Tony?

Tony: OK, but didn't the teacher give us a world file to use? Maybe we should open it in setUp also.

Lisa: Right. But we'd better clear the world between tests. Remember that the framework runs each test separately and runs setUp between each test. (typing...)

public void setUp()
{	World.reset();
	World.readWorld("stairworld.kwld");
	karel= new StairClimber(1, 1, East, 0);	
} 

Ok. Let's compile it. (Hits the build button... Sees compile errors.). Oops. We haven't got the StairClimber class yet so it won't compile. Let me create the class outline ( Opens a new file. Types the following. Saves as StairClimber.java)

package kareltherobot;

public class StairClimber extends UrRobot
{  

	public StairClimber(int street, int avenue, Direction d, int beepers)
	{   super(street, avenue, d, beepers);
	}

	
}
 

Ok, now we should at least compile ok. (Tries, and succeeds). Now let's try to run it. I'll make kareltherobot.StairClimberTest the main. (Does that in their IDE and hits the run button. -- The following appears on their screen.)

Oops. A red bar. Well, I guess that is expected since we don't have any actual tests yet and that is what the message says. Want to drive?

Tony: Sure (takes the keyboard from her). Leaving me the hard stuff huh? (Lisa laughs). Ok. We need to climb three stairs. Suppose we think about climbing just one. How will a robot climb the first stair? I guess we just have it turn left, then move then turn right and then move again. That should put it on the next step. Before we start, our front should be blocked and we should be facing east, right?

Lisa: Yes, and the robot should be running. Oh wait, we don't have a turnRight instruction. Lets do that first. How do we test that?

Tony: Well a right turn from East should leave us facing South. Let's look at the JavaDoc for the test framework again. (Looks...) Our set up started us facing East, remember. Let's try this. (Adds the following to StairClimberTest.java).

 	public void testRightTurn()
	{	assertFacingEast(karel);
		assertRunning(karel);
		karel.turnRight();
		assertFacingSouth(karel);
		assertRunning(karel);
	}

That looks ok, but it won't even compile until we put in the turnRight instruction in StairClimber. (Adds the following to StairClimber.java).

 	public void turnRight()
	{	turnLeft();
		turnLeft();
	}

OK. Let's run it.

Lisa: Well, I think you need three turnLeft's but let's run it anyway just to see what happens. Hit the build button again.

Tony: OK. (Hits build) Remember that we don't need to run it again, just hit the run button on the test gui.

Ok, that was expected and it also says we aren't facing south and that the error is on line 22 of our test file. Cool. (types...)

 	public void turnRight()
	{	turnLeft();
		turnLeft();
		turnLeft();
	}

That should be better. (Hits build in the IDE and then run in the test GUI...)

Cool, our first green bar.

Lisa: Well, yeah, but it isn't much of a test, since we only tested turning from East to South. Here, let me. (Takes keyboard... types...)

 	public void testRightTurn()
	{	assertFacingEast(karel);
		assertRunning(karel);
		karel.turnRight();
		assertFacingSouth(karel);
		assertRunning(karel);
		karel.turnRight();
		assertFacingWest(karel);
		assertRunning(karel);
		karel.turnRight();
		assertFacingNorth(karel);
		assertRunning(karel);
		karel.turnRight();
		assertFacingEast(karel);
		assertRunning(karel);
	}

Now we have tested turns around the compass. (Builds, tests, gets the same green bar as above.) Now THAT is cool. Ok. Let's go back to climbing one stair. If we start at 1, 1, we should wind up at 2, 2, facing the same direction, right? (Types...)

	public void testClimbOneStair()
	{	assertFacingEast(karel);
		assertRunning(karel);
		assertFrontIsBlocked(karel);
		assertAt(karel, 1, 1);
		karel.climbOneStair();
		assertFacingEast(karel);
		assertRunning(karel);
		assertAt(karel, 2, 2);
	}
 

So we start out facing East at 1, 1 and end up still running at 2, 2 and still facing east. (Tries to compile and fails, no ClimbOneStair in StairClimber.java...)

Tony: (Takes keyboard...) Ok, here is the climb code. (Types...)

	public void climbOneStair()
	{	turnLeft();
		move();
		turnRight();
		move();
	}

Now we should get a green bar. I hope. (Builds again and hits the Run button on the GUI. )

Ok, great. Now it says it ran 2 tests. What is the Test Hierarchy tab for? (Clicks it.)

Oh, I see. It lists the tests we ran and passed. Wicked Cool.


The two of them continue for a while and create two new methods and tests for them: ClimbStairs and GetBeeper. When they get their final green bar for the four tests, they are pretty assured that they have solved the problem. Note, however, that they haven't actually run any robot programs. They have been running the test framework. They therefore haven't gotten any feedback from moving robots. Now it is time to see their robot in action. Here is the code they wound up with.

StairClimberTest.java

StairClimber.java

They next write a graphical tester (RobotTask) that they can run under the KarelRunner driver:

 

package kareltherobot;

class Test	implements  RobotTask
{
	public void task()
	{	World.reset();
		World.readWorld("stairworld.kwld");
		StairClimber karel = new StairClimber(1, 1, East, 0);
		karel.getBeeper();
	}
}

They then set KarelRunner as the main and run again (Run from the IDE, not the test GUI). Here are some snapshots of their test run.

Climbing the second step. About to turn right.

Half way through the turn right.

Completing the second step.

Climbing the third step.

All done.

Interestingly, however, if they had put World.setVisible(true); into the setUp method of the StairClimberTest class, they would have seen both the progress bar in the test GUI as well as the running robot executing the test code. You can have it all.


Notes:

The test class should always have the structure of our skeleton, and always needs a setUp method as well. Objects to be tested should be fields of this class, but they should be initialized in setUp, not in the declarations.

You always need the constructor shown, and a main. In the main you can use the awtui instead of the swingui if you like. It is a bit less sophisticated.

All of your test methods should start with "test" as in testRightTurn. The test infrastructure needs these name prefixes to work as easily as it does here, though there are other options.

Try to get in the habit of writing the test before you write the code. Try to get in the habit of writing complete tests, not just the simplest case. Write several tests for a method if you need to.

You can, of course, write tests for sequences of messages, not just for a single one. Here we separated out the methods of the StairClimber class for individual testing, but in some cases you also want to test sequences of messages.

For a simpler example of testing see UnitTest.java. It tests only the behavior of UrRobots in an empty world.

Note that the test code is completely separate from the robot code. We don't put assertions in the code we are testing, but in the separate tests. This has two implications. First is that we only test the code "externally" based on what it does, not its internals. Second, an error in a test, won't harm the robot code, though it may result in an inadequate test of it.


Testing Exercise

Here is an exercise that you can use to test your testing skills. Consider the following candidate for turnRight. Notice that it does make the needed number of turnLefts and leaves you on the corner at which you start.

 	public void turnRight()
 	{	move();
 		turnLeft();
 		move();
 		turnLeft();
		move();
		turnLeft();
		move();
	}

Can this instruction fail? Note that it doesn't always fail. Start a robot at 3, 3, in an empty world, for example. Can you write a test for turnRight that will catch this error no matter where your robot starts?

Hint. Assertions are not enough. You need to think about the test as a whole. Think about a world designed for the test itself.

 

Last updated: August 20, 2004