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 Double
s 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:
- New JUnit Jupiter test
- Package:
warmup3
- Name:
DataSetTest
- Class under test:
warmup3.DataSet
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)
.
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).
-
The first imports the assertion methods we use to write tests.
-
The second imports the
@Test
annotation which we use to indicate that the method should be run as a test.
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.
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.
assertEquals(expected, actual [, message])
— asserts thatactual
is equal toexpected
assertNotEquals(unexpected, actual [, message])
— asserts thatactual
is not equal tounexpected
assertTrue(condition [, message])
— asserts thatcondition
istrue
assertFalse(condition [, message])
— asserts thatcondition
isfalse
assertNull(actual [, message])
— asserts thatactual
isnull
assertNotNull(actual [, message])
— asserts thatactual
is notnull
assertThrows(expectedType, executable [, message])
— asserts thatexecutable
throws an exception of classexpectedType
; see below for detailsfail(message)
— fails the test with the given message
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 DataSet
s 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.
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.