Example 1: Building a Gradebook

SIGCSE 2006 Workshop Companion Web Site

Goal

In this example, we will be using a simple CS1-style problem to introduce you to the basics of writing test cases. For context, imagine that you are going to develop a pair of classes that work together to represent a rudimentary gradebook. One class will represent a student's individual information, while another will represent the gradebook itself.

Note: If you are unable to complete this example, or if you are unfamiliar with Java syntax and need a bit of help, the Gradebook-final directory contains completed versions of the source files for this example.

A C++ version of this example is also available.

Learning Objectives

Prelude: Start Up Your IDE

This example is written in Java. You can work on this example using any IDE or environment you prefer. Instructions for some environments are provided here for convenience.

If you are using BlueJ

  1. Download the Source Files

    Create a subdirectory to hold your project. Download the source files for this example from the Gradebook folder and place them in your new project directory.

  2. Create Your Project

    Open BlueJ. Choose the Project->Open Non BlueJ... menu command and select your project subdirectory.

If you are using Eclipse

  1. Open Eclipse

    Start Eclipse and select the workspace you want to work in.

  2. Create a New Project

    Use the File->New->Project... command to create a new Java project called Gradebook.

  3. Add JUnit to the Classpath

    A newly created project in eclipse does not include JUnit support by default, so we must add it manually. Note that if you are creating a project yourself, the first time you create a new JUnit class, the wizard will take care of this step for you. Here, however, you will be importing existing files, so we must add JUnit to the classpath manually.

    Right-click your Gradebook project in Eclipse's package explorer view and click Properties. Select Java Build Path on the left of the properties dialog, and then select the Libraries tab on the right. Click the Add Variable... button. Select the JUNIT_HOME variable from the list and click Extend..., then click on junit.jar. Click OK twice.

  4. Download the Source Files

    Download the source files for this example from the Gradebook folder and place them in the Gradebook project folder within your workspace. Right-click on your Gradebook project in Eclipse's package explorer and choose Refresh so that Eclipse will see the newly added source files.

If you are Working From the Command Line

  1. Download JUnit

    Download JUnit from http://www.junit.org/. Place the JUnit jar file where you can conveniently add it to your classpath while compiling and running programs. Read the JUnit documentation to familiarize yourself with how to run JUnit test classes using the gui runner.

  2. Download the Source Files

    Create a subdirectory to hold your project. Download the source files for this example from the Gradebook folder and place them in your new project directory.

Part A: What Do Test Cases Look Like?

These instructions are written assuming you are using Eclipse, but you should be able to follow along with other IDEs (or even on the command line) if you prefer.

Procedure

  1. Open the Gradebook project

    The left pane of the Eclipse window shows a tree of the projects in your workspace. The top-level nodes in this tree represent separate "projects" in your workspace. Expand the Gradebook project so that you can see its contents.

  2. Open the Student.java file

    Double-click the Student.java file to open it in an edit window. This class provides a simple model for a student entry in a gradebook. Read the Javadoc comments to familiarize yourself with the methods.

  3. Open the StudentTest.java file

    Double-click the StudentTest.java file to open it in an edit window. This class provides a JUnit test class. By convention, we'll keep all the test cases for class Foo in a test class called FooTest.

  4. Add a test case

    In JUnit, a test case is simply a public void method with no parameters and with a name that starts with "test...". Add a test case by inserting the following method at the end of the StudentTest.java class (you will find a declaration with an empty method body already in the file):

        public void testName()
        {
            Student fred = new Student( "fred" );
            assertEquals( "fred", fred.name() );
        }
    

    The first line in this method creates an instance of the class under test (a.k.a., CUT)--the Student class. We can use this object to execute one or more methods, and then state what we expect the resulting situation to be.

    The second line is where we define our expected behavior. In this case, we are calling the name() accessor, and our expected output is the string "fred".

    In JUnit, we always phrase the "expected output" of a test case in terms of one or more assertions. One of the most common assertions is assertEquals() which takes an expected value followed by a test expression, and asserts that the two are equivalent (that is, test positive using the equals() method, in Java). If you are interested, you can view the full list of assertions supported in JUnit test classes.

  5. Run your test

    Save the modified StudentTest.java file. Select the StudentTest.java file in the left pane. From the Run menu, select Run As->JUnit Test. You can see the results in the JUnit tab that appears in the Eclipse window. Click on the JUnit tab if it does not automatically jump to the foreground.

  6. Add another test case

    Based on the first test case as an example, add a second test case to StudentTest.java. This second test case also should create a student named "fred", and then check that the number of assignments recorded so far is initially zero.

  7. Run your tests

    Save the modified StudentTest.java file. Select the StudentTest.java file in the left pane. From the Run menu, select Run As->JUnit Test. You can see the results in the JUnit tab in the lower pane of the Eclipse window.

  8. Remove the common setup code from your test cases

    Notice that both test cases work on Student objects that start in the same state. We can extract the common setup code to create a test fixture. A test fixture is simply a collection of objects that serves as the starting point for all of the test cases in your test case class. You can configure this collection of objects to be in any state you want--the key is that every test case will begin with the same collection of objects in exactly the same starting state.

    JUnit allows a test class to define a setUp() method for just this purpose. To use it, first add a new field to the class:

        private Student fred;
    

    We won't initialize this field in the constructor (which will only be executed once). Instead, we will initialize it in the setUp() method that is already declared in the class. Add this line to the body of the setUp() method:

        fred = new Student( "fred" );
    

    Remove the declarations of the local fred variables (and their corresponding initializations) from the two test cases you have written so far. Now your test case methods will refer to the class field rather than local variables. JUnit takes care of automatically executing setUp() to re-initialize the test fixture before it tries to run each individual test case in the class. Re-run your tests (The JUnit tab provides a toolbar-style button on the far right of its title bar to re-run the most recently executed tests.

  9. Add a third test case

    Add a new test case that asserts that fred's assignment average (before we've added any assignments) is zero. Run your tests. Notice the failure indication shown on the JUnit tab. The "failure trace" shows the location of the assertion that failed and the reason for the failure. You can use the iconic button on the right just above the failure trace output to see both the expected and actual results where the mismatch occurred.

  10. Fix the simple bug

    If you have time, feel free to fix the bug in the assignmentAverage() method. You may wish to add a few more test cases for the assignmentAverage() method to confirm that it works as intended.

Part B: Using Test Cases to Clarify Assignment Specifications

Procedure

  1. Open the AssignmentSpecificationTests.java file

    Now that you have seen how to write a few basic tests, let's look at how an instructor might use test cases to clarify an assignment specification.

    Open the AssignmentSpecificationTests.java file. This file is an example set of test cases that an instructor might provide as part of a programming assignment in order to specify the required/intended behavior for the classes and methods students must write. Such a test suite provides a greater degree of rigor and precision than a plain English behavioral description.

  2. Read the assignment specification test cases

    Read through the test cases in the AssignmentSpecificationTests class. Note that each test case performs two functions:

    • It presents a concrete example of how one or more methods might be used in practice (i.e., shows sample calls).
    • Its assertions describe the expected behavior that should result from such sequences of action.

    Taken together, the entire test suite also provides the student with a yardstick by which they can measure the completeness and correctness of their own solution.

  3. Run the assignment specification test cases

    Select the AssignmentSpecificationTests.java file in the resource view on the left. Use the Run->Run As->JUnit Test menu command to run the tests.

  4. Identify the failure(s)

    Use the JUnit results presented in the JUnit tab at the bottom of the Eclipse window to identify which test cases failed, and why. Can you identify the cause of the failure(s)?

  5. Repair the failure(s)

    Repair the cause(s) of the failure(s) you have found. Re-run the test suite to ensure your repair conforms to the expected behavior.

Note: While the assignment specification test suite in this project covers all the basics, it is not "complete", in that it is still possible to write a solution that contains bugs but still passes these tests. Writing test sets that fully cover all possibilities is hard whether you are an instructor or student. Can you spot any holes in the tests provided?