Lab: Unit Testing
Steven J Zeil
This is a self-assessment activity to give you practice in working with Java unit test frameworks. Feel free to share your problems, experiences, and choices in the Forum.
1 Set Up Your Project
-
Start with your refactored code from this earlier lab. Close your IDE if you have it open.
-
Download and unpack this code into a convenient directory outside of your project.
-
Copy the
testing/*.java
files into the appropriate source code directory in your project (Pay attention to the package names!). -
Move the
lib/
directory (the entire directory, not just its contents) to the top-level directory (the one containingsrc/
) of your project. -
Next, we want to set up this project to support a unit test framework. Open the project in your IDE.
-
In VSCode, jars that are in the
lib/
directory are automatically added to the project. In this case, the JUnit and Hamcrest libraries should be detected. -
In Eclipse, you will need to configure your project’s Build Path. Right-click on the project name and select
Build Path
, thenConfigure Build Path...
. Go to the Library Tab,- Select “Classpath”, then click
Add Library...
. Select Junit, Next, JUnit 5, Finish. - Select “Classpath”, then click
Add External Jars...
. From thelib
directory, select the “hamcrest” jar. - Go to the “Order and Export Tab”. Move hamcrest above Junit 5.
- Select “Apply and Close”.
- Select “Classpath”, then click
We will later make a point of automating this setup so that libraries get imported without all this manual manipulation.
-
-
Examine
TestInterval.java
andTestRanges.java
. They should compile without errors. -
Run the tests.
-
In Eclipse, right-click on VocabTest.java and select
Run as...
, then “JUnit test”. -
In VSCode, click the testing icon on the left (the laboratory flask). Hover your mouse over the package name, and click the triangular Run button.
The test should run successfully.
-
-
Try changing one of the test assertions “…is(true)…” to “…is(false)…”. Rerun the tests and observe what a test failure looks like in your IDE.
Change it back to “…is(true)…”.
-
JUnit tests can be run from the command line. In a terminal,
cd
to your project directory and give the following command:java -jar ./lib/junit-platform-console-standalone-*.jar -cp '.:lib/*' -c edu.odu.cs.TestInterval
You should see that the test runs and is successful.
This is, admittedly, a fairly awkward way to run tests. Again, when we later cover build management, we will eventually see cleaner ways of integrating external libraries into projects
-
Finally, read the tests in
TestInterval.java
.Look in particular at
testBelow()
. Do you see how it follows the pattern of invoking a mutator function (thebelow()
function) and then examining the effects of that call on each of the accessor functions in theInterval
class?Do you understand what each of the assertions says about the value of the
Interval
objectbelow75
following the most recent mutation?
2 Write Some Tests
Look now at TestRanges.java
. Again, make sure that you understand what is going on in the test code.
The one subtlety in this code is that Ranges
is “iterable”, meaning that it provides an iterator over a sequence of objects, in this case, a sequence of Interval
values.
Normally you would employ such an iterator like this:
Ranges r = new Ranges(...);
⋮ --- one or more r.remove(...) calls
Iterator<Interval> iter = r.iterator();
while (!iter.hasNext()) {
Interval interval = iter.next();
doSomethingWith(interval);
}
or, more simply, the iterator can be used implicitly in a range-based for loop like this:
Ranges r = new Ranges(...);
⋮ --- one or more r.remove(...) calls
for (Interval interval: r) {
doSomethingWith(interval);
}
As it happens, Hamcrest provides a convenient contains
matcher for comparing the entire sequence of objects provided by an iterable object to an array or other sequence.
Interval[] expected = {interval1, interval2, interval3};
assertThat (r, contains(expected)); // r's iterator must provide a sequence of 3
// objects equal to the ones in the array expected
The tests in TestRanges.java
are not complete. Try to finish them.
Question: What new @Test functions will you need?
-
Add your tests to
TestRanges.java
. Compile and run them.They should compile, and should pass. Fix them if necessary.
-
Copy the file
Ranges.java
toRanges.java.original
. The copyvariations/Ranges.java.instrumented
into yoursrc/main/java/edu/odu/cs/Ranges.java
directory, replacing the oldRanges.java
.(In Eclipse, refresh your project again after doing so.)
This file has been instrumented to report on how well your tests exercise the mutator and accessor functions during testing.
-
Compile and run the tests again. At the end of testing, you will get a report indicating whether any mutators were not checked with one or more accessors.
Fix your tests, if any combinations are reported as not having been tested.
3 Catch Some Bugs
-
Copy the file
variations/Ranges.java.bug1
intosrc/main/java/edu/odu/cs/Ranges.java
.(In Eclipse, refresh your project again after doing so.)
This file contains a buggy implementation of class
Ranges
. -
Compile and run the tests again. Did your tests catch the bug? If not, perhaps your tests are not aggressive enough.
One thing to consider is how you might apply some of the conventional blackbox testing strategies (functional coverage, boundary value testing, & special value testing) to testing the
remove
function. For example, you might consider functional cases of- Removing an interval that overlaps the low end of a range.
- Removing an interval that overlaps the high end of a range.
- Removing an interval from the interior of a range.
For boundary/special cases, you might consider
- Remove an interval that covers the entire range.
- Remove an interval that lies entirely outside the range.
- Remove an empty interval from the range.
Now, you can try to cram all of these inside a single
testRemove
function, but I think you get something a great deal more readable if you put each case inside a separate, appropriately named,@Test
function. -
Repeat the above steps with
variations/Ranges.java.bug2
-
When you are done, restore the original
Ranges.java
file from the copy you saved asRanges.java.original
.
If you would like to see my full test of tests for this class, you can see it here:
package edu.odu.cs;
import org.junit.jupiter.api.Test;
//import static org.junit.jupiter.api.Assertions.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
/**
* Test of the Ranges class
*/
public class TestRangesComplete {
double precision = 0.001;
@Test
public void testConstructor() {
Ranges ranges = new Ranges(1.0, 100.0);
assertThat(ranges.sum(), closeTo(99.0, precision));
Interval[] expected = { new Interval(1.0, 100.0) };
assertThat (ranges, contains(expected)); // checks ranges.iterator()
}
@Test
public void testSubtractLowEnd() {
Ranges ranges = new Ranges(10.0, 100.0);
Interval interval = new Interval(0.0, 20.0);
ranges.remove(interval);
assertThat(ranges.sum(), closeTo(80.0, precision));
Interval[] expected = { new Interval(20.0, 100.0) };
assertThat (ranges, contains(expected));
}
@Test
public void testSubtractHighEnd() {
Ranges ranges = new Ranges(10.0, 100.0);
Interval interval = new Interval(90.0, 110.0);
ranges.remove(interval);
assertThat(ranges.sum(), closeTo(80.0, precision));
Interval[] expected = { new Interval(10.0, 90.0) };
assertThat (ranges, contains(expected));
}
@Test
public void testSubtractInterior() {
Ranges ranges = new Ranges(10.0, 100.0);
Interval interval = new Interval(20.0, 90.0);
ranges.remove(interval);
assertThat(ranges.sum(), closeTo(20.0, precision));
Interval[] expected = { new Interval(10.0, 20.0), new Interval(90.0, 100.0) };
assertThat (ranges, contains(expected));
}
@Test
public void testSubtractAll() {
Ranges ranges = new Ranges(10.0, 100.0);
Interval interval = new Interval(9.0, 110.0);
ranges.remove(interval);
assertThat(ranges.sum(), closeTo(0.0, precision));
assertThat (ranges.iterator().hasNext(), is(false));
}
@Test
public void testSubtractNone() {
Ranges ranges = new Ranges(10.0, 100.0);
Interval interval = new Interval(101.0, 110.0);
ranges.remove(interval);
assertThat(ranges.sum(), closeTo(90.0, precision));
Interval[] expected = { new Interval(10.0, 100.0)};
assertThat (ranges, contains(expected));
}
@Test
public void testSubtractEmpty() {
Ranges ranges = new Ranges(10.0, 100.0);
Interval interval = new Interval(51.0, 50.0);
ranges.remove(interval);
assertThat(ranges.sum(), closeTo(90.0, precision));
Interval[] expected = { new Interval(10.0, 100.0)};
assertThat (ranges, contains(expected));
}
}
That may look like a lot of code, but note that much of it is copied and pasted from the first testRemove...
function, with only minor tweaks to each copy.