CS-151 Labs > Lab 3. My ArrayList


Warmup

As always, create directory lab3 in your cs151 folder .

Now create a new Java project called ‘lab3’, and save it in the lab3 directory. Add a new class called DataSet with package warmup3. Copy all of the code from here into DataSet.java.

DataSet is a simple class that contains an ArrayList of Doubles and can compute some very simple statistics on the data: min, max, mean, and median.

Testing with JUnit

Let’s explore an idea of writing automated tests to test the correctness of our programs. We will use JUnit testing framework which allows to write and execute automated tests. We create a test for each method and re-run all the tests every time we add changes to our class. This is done to make sure that nothing in the code has been broken by changes.

Create a new JUnit test case via File > New > JUnit Test Case. As long as you do this while DataSet.java is open in the editor and its name is highlighted in the project explorer, Eclipse would fill in all the correct values for you:

JUnit test method stub generator dialog

Click Next (not Finish) and Eclipse will ask what methods you want to create test method stubs for. Select all the methods of the DataSet except for the constructor DataSet() and add(double).

JUnit test method selection

Eclipse might tell you that “JUnit 5 is not on the build path.” Click OK to add the JUnit 5 library to the build path.

This will create a new class named DataSetTest. Commonly, when writing unit tests for a class Thing, the test class is named ThingTest.

There are two import lines in DataSetTest.java (Eclipse may have collapsed them into a single line; if so click the small + icon to the left of the import line to expand it).

If you get errors on the import lines after Eclipse created the class, you probably accidentally added the module-info.java file. Delete it and everything should work.

Before we do anything else, let’s run the auto-generated test stubs by right-click and selecting Run as JUnit test from the context menu of DataSetTest.java file. This will bring up the JUnit report tab with the following:

Runs: 5/5 Errors: 0 Failures: 5

Each of the auto-generated fail() calls caused these tests to fail.

JUnit results

Let’s start by implementing an actual test for the size() method. Inside your testSize(), delete the fail(…); and create a new DataSet ds = new DataSet();. Start by testing that it starts out with no elements. Enter:

assertEquals(0, ds.size());

on the next line. The call to assertEquals(expected, actual) tests that actual is equal to expected and if it is not, then the test will fail. There are many similar assertion methods. Most of them take an optional String message to be displayed if the assertion fails. Here is a partial list of the supported methods where the [, message] indicates the optional String argument.

Check out the JavaDoc for a list of all possible assertions.

Run the tests again. You should see

Runs: 5/5 Errors: 0 Failures: 4

Go ahead and add a few more elements to the data set and test the size by adding appropriate assertEquals() statements.

So far, everything is looking good. Let’s see what happens when the assertion fails. Change your test to be deliberately wrong. For example:

DataSet ds = new DataSet();
ds.add(1.5);
ds.add(2.0);
assertEquals(1, ds.size());

When you run the tests, it had better fail! Go ahead and do so now. When you run the tests you should see the method size() in the list of failures. On the bottom left quadrant of the Eclipse window, you should see Failure Trace. If you double click on the line that says org.opentest4j.AssertionFailedError, it’ll show you what the actual and expected values were.

Okay, fix your test and now write a test for max(). Make sure you add some positive and negative numbers to your DataSet. Run your tests. You should have one less failures than before.

Create a test for min(). You can duplicate your test for max(), give it an appropriate name, and modify any assertion statements appropriately.

If all the tests you have written up to this point pass, then I have some bad news: your testing was insufficient. Go back to your tests for max and min. Create multiple DataSets and assert the values of max() and min(). Try creating one that only has positive numbers; one that only has negative numbers; one that only has the same number. Try to think of any other edge cases and test those.

Which values inserted into your DataSet caused the method to fail? Report your experience in the readme.txt.

Debugging

It’s now time to debug what went wrong. For that, we can use the Eclipse debugger. Find the line of the assertion that failed. Double click on the line number. This will cause a small blue dot to appear. This indicates a “break point.” When you run the code in a debugger, each time a break point is reached, the debugger will stop and give you control. From the Run menu, select Debug. (Select “Switch” if Eclipse asks if you want to switch to the debugging view.) This will cause the tests to run until the line with the break point is reached.

In the new debugging view, there are several important things to notice. The left pane shows a “stack trace.” This is a list of all of the methods that were called to reach the current location. You’ll notice there are a lot of them. Most of them are irrelevant; they’re part of the JUnit testing infrastructure. The top line should be DataSetTest.testXXX() corresponding to the method the breakpoint is in.

Debug view

The right pane has three tabs. The “Variables” tab shows the values of the variables that are in scope. This is invaluable for figuring out what’s going on. No more adding print statements to see what the values of variables are, the debugger will show you!

At the top of the window are a few nondescript buttons. From left to right, they are Resume, Suspend, Terminate, Disconnect (we won’t need this), Step Into, Step Over, and Step Return. Right now, Eclipse is in single-stepping mode meaning we can step through statements one at a time. Step Over will execute the statement and move to the next. Step Into does the same unless the statement includes a method call in which case it will step into the method so that we can single step inside of it.

Click the Step Into button (or press F5) to step into the method that failed the assertion. Step through the method using Step Over (or F6). After each step, examine the contents of the Variables tab on the right to make sure that the variables contain the values you expect.

Once you have figured out the bug, you can stop debugging by clicking Terminate. Fix the bug in DataSet.java and rerun your tests. Hopefully, they are passing now. If not, start the debugger again and give it another go. Debugging is an iterative process. It’s not uncommon to need to spend more time debugging code than writing it.

To exit the Debug view, simply click on the Project Explorer tab.

Remaining tests

Implement tests for mean() and median(), fixing any bugs in the code your tests uncover. Recall that the mean of n numbers x1 through xn is (x1 + x2 + … + xn)/n. In contrast, the median is the middle value of n sorted numbers (the mean of the two middle values if n is even). E.g., the median of {5,3,100} is 5. The median of {5, 3, 100, 6} is 5.5. Finish the tests. Make sure all of your tests pass.

Exceptions

Finally, we need to handle some exceptional cases. What should happen when somebody calls max(), min(), mean(), or median() and the DataSet is empty?

Let’s make our methods throw an IllegalStateException if the DataSet is empty.

Add the following code to the beginning of each of those four methods:

if (data.isEmpty()) {
    throw new IllegalStateException("DataSet is empty");
}

Now we just need to add some assertions to our tests that tests if the Exception is thrown. To test that an expression throws a particular exception, we need to use assertThrows with some new syntax.

Here’s the test for max():

assertThrows(IllegalStateException.class, () -> ds.max());

The first argument, IllegalStateException.class, says that the expression under test should throw an IllegalStateException. The second argument, () -> ds.max(), is Java’s syntax for an anonymous method that takes no arguments and simply computes ds.max().

Add similar assertions to each of the four test methods in DataSetTest.java. Make sure you change ds.max() to the appropriate method for each test.