After the Java 25 upgrade two days ago, I went back to the tests. Well, the two we’d added around URL handling during the migration. The original application didn’t have any, even though JUnit was already sitting in the POM.
Bob’s Java package has a separate Java Unit Testing workflow, and I wanted to see what it would do with that starting point. I picked the weather-data class. Six cities, six JSON files. Ask for Paris, get Paris. Small enough that I could read every generated test and check whether it caught a mistake.
Bob’s first proposal included a framework upgrade and servlet tests. I trimmed that back before continuing. Then the candidate selection proposed all four classes, so I had to narrow that down too. The test code itself needed less attention than those two decisions.
I’m using a separate copy of the original Java 8 revision here. Keep your Java 25 project if you followed part one; you won’t need to undo any of that work.
Get the project ready
The setup in part one covers Bob, access to the Java premium package, Maven, Git, and selecting a JDK through SDKMAN. You can start there if you’re joining with this article. For this run I used Bob 1.126.0+bob2.1.0, Java package 1.0.3, Maven 3.9.16, and Liberica JDK 8.0.462.
The testing workflow prepares a strategy before generating tests. It can also add JaCoCo to record which production code the tests execute. We’ll get to that report after the tests are running.
Clone the IBM appmod-resorts sample into a new directory and select the same starting revision:
git clone https://github.com/IBM/appmod-resorts.git appmod-resorts-unit-tests
cd appmod-resorts-unit-tests
git switch -c tutorial/tests c373ec12ed63c0114763c3b28d0140d5b4829770
git remote remove origin
git status --shortRemoving the remote keeps the work local. Open the directory containing pom.xml in Bob and select Java 8 in its terminal:
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk use java 8.0.462-librca
java -version
mvn -version
mvn -B clean verifyUse your own JDK 8 identifier if you’re using a different one. Do check mvn -version before building: this copy still has WAR plugin 2.6, the one that failed under Java 25 in part one. With Java 8, I got:
[INFO] No tests to run.
[INFO] BUILD SUCCESSThat’s the starting point. The WAR builds, no tests run, and there’s no coverage report yet. Calling this zero-percent coverage would suggest we’d measured it.
Building rewrites the example WAR that this repository tracks. Restore that generated file before starting Bob, then check that the working tree is clean:
git restore --source=HEAD -- data/examples/modresorts-1.0.war
git status --shortThe two classes I picked
DefaultWeatherData takes a city name, finds a JSON file in src/main/resources, and returns its contents. All six files are in the repository. We can test that without starting a server or getting a weather-service account.
There’s one detail to keep in mind when Bob writes the tests: the class is package-private. Its test belongs in src/test/java/com/ibm/ta/modresorts, with the matching package declaration. That lets it call the class without changing production visibility.
I added ExceptionHandler to the scope as well. It wraps an error in ServletException, so I wanted to check that the message and original cause survive. We’d already dealt with that handler during the URL change.
WeatherServlet can wait. It reads an environment variable and opens HTTP connections; part one needed a prepared HTTP response to test its live-weather method. I wanted to get through this run with the existing source as it was.
Let Bob prepare a strategy
Choose Start Workflow → Java Unit Testing in the sidebar. Bob found no coverage tool and offered to add JaCoCo, then started on the strategy. I supplied the scope in the chat:
Generate deterministic unit tests for DefaultWeatherData and ExceptionHandler. Cover all six city JSON files, invalid inputs, and exception cause preservation. Put tests in the matching packages. Do not call external weather services or require a servlet container. Assert meaningful returned values and behavior; do not weaken production behavior to satisfy tests. Keep Java 8 compatibility and the existing JUnit 4 dependency unless a change is necessary. Run tests and JaCoCo, and report selected-class and whole-project coverage separately. Review the strategy before generating tests. Local changes only. Keep Git Flow disabled. No push or PR.
Also tell Bob which JDK 8 installation to use for Maven. Selecting a JDK in your terminal doesn’t guarantee that every command Bob starts will inherit that selection.
Bob wrote UNITTEST.md in the project root. Its proposal recommended JUnit 5, Mockito, servlet tests, and coverage thresholds. That’s a fair amount of additional work for loading six files and checking an exception.
I replaced that proposal with my reviewed UNITTEST.md. I kept JUnit 4.13.1, which already provides Assert.assertThrows, and asked for JaCoCo 0.8.12. No mocking library was needed for either class. I also removed the coverage thresholds. I wanted to see what this small suite covered before choosing a number for the build to enforce.
The main requirement I added was to parse the returned JSON and check current_observation.display_location.city. These are the input and output pairs I checked against the committed files:
Constructor inputExpected JSON cityParisParisLas_VegasLas VegasSan_FranciscoSan FranciscoMiamiMiamiCorkCorkBarcelonaBarcelona
Notice the underscores in the inputs and spaces in the displayed names. The San Francisco file also has its own spelling: sanFrancesco20180810Weather.json. I left that alone.
Invalid input needed a little more definition. Null throws UnsupportedOperationException with City is not defined. Unsupported cities, empty and whitespace-only strings, and wrong-case names such as paris also throw UnsupportedOperationException. The constructor doesn’t trim or normalize anything. Tests that expect it to accept those names would be asking for a behavior change.
For ExceptionHandler, the strategy asks for both branches. A null cause should give us ServletException with the supplied message and no cause. With an existing exception, it should preserve the message and that exact exception object.
Bob still selected all four classes
With JaCoCo configured and the strategy saved, I restarted the workflow. On Select Tasks, I enabled Generate Unit Tests and Run Code Coverage. Keep Re-generate Unit Test Strategy off here, otherwise you’re asking Bob to replace the file you just reviewed. I left Enable Git Flow off too.
Keep the reviewed strategy and enable generation plus coverage. Git Flow stays disabled in this copy.
Candidate Selection Strategy offered All classes and Changed classes in my installed version. The production sources hadn’t changed, so I chose All classes. The next confirmation proposed four classes in two batches, even though I’d named two in the strategy.
I narrowed it down through the chat:
Narrow generation to only DefaultWeatherData and ExceptionHandler as specified in the reviewed UNITTEST.md. Do not generate WeatherServletTest or ConstantsTest. Use the reviewed strategy, keep JUnit 4.13.1 and Java 8, and generate and run those two test classes now.
Bob then generated the two test classes I’d asked for.
What came back
Bob generated 13 tests in DefaultWeatherDataTest.java and two in ExceptionHandlerTest.java. The six city tests share a helper that loads and parses the response. Here’s the assertion at the end of that helper:
JSONObject json = new JSONObject(weatherJsonString);
JSONObject currentObs = json.getJSONObject("current_observation");
JSONObject displayLoc = currentObs.getJSONObject("display_location");
String cityName = displayLoc.getString("city");
assertEquals("City name in JSON display_location does not match",
expectedCityName, cityName);The San Francisco test calls assertCityWeather("San_Francisco", "San Francisco"). The other five follow the same pattern. Bob kept non-null and nonempty checks in the helper too.
The remaining seven weather tests cover null and the invalid strings, including San Francisco and Las Vegas with spaces. In the exception tests, Bob used assertThrows, checked the message with assertEquals, and used assertSame for the cause. I read both files and ran them without editing the generated Java.
Run the tests and check the build
Bob ran clean verify under Java 8 and reported 15 passing tests. I then ran the selected suite independently in the terminal:
mvn -B -Dtest=DefaultWeatherDataTest,ExceptionHandlerTest test
mvn -B clean verify
git diff -- pom.xml src/main/java src/main/resources
git status --shortAgain, 15 tests passed with no failures, errors, or skips, and the clean build packaged the WAR. The only POM change adds JaCoCo’s prepare-agent goal and binds report to verify. With that configuration, clean verify writes a fresh target/site/jacoco/index.html without a separate report command.
There were no production-source or resource changes. Git also listed the strategy, the new tests, and the rebuilt tracked WAR. Remember that untracked tests won’t appear in git diff; they’re in git status until you add them.
Paris gets Miami’s weather
I made one temporary change to DefaultWeatherData.java: the Paris branch now loaded Miami’s file. You can try the same edit in your isolated copy:
if (Constants.PARIS.equals(getCity())) {
- dataFileName = Constants.PARIS_WEATHER_FILE;
+ dataFileName = Constants.MIAMI_WEATHER_FILE;Run just the Paris test:
mvn -B -Dtest=DefaultWeatherDataTest#testParis testMy run failed with one assertion failure:
City name in JSON display_location does not match
expected:<[Paris]> but was:<[Miami]>The file exists, the string isn’t empty, and the JSON parses. A test checking only those things would have passed. This one failed because it expected Paris.
Put Constants.PARIS_WEATHER_FILE back on that line and run mvn -B clean verify again. All 15 tests passed after I restored it, and Maven reported BUILD SUCCESS. Use that full run for the coverage report below.
About that coverage number
Open target/site/jacoco/index.html to explore the report. These are the instruction and branch counters from my final clean run, with percentages rounded to two decimal places:

The Total row includes the whole project. JaCoCo displays whole percentages here; the underlying counters give 33.85% instruction coverage and 42.86% branch coverage.
Click com.ibm.ta.modresorts to see te individual classes:

DefaultWeatherData has 91% instruction and branch coverage in this view. WeatherServlet is still at zero.
The two classes look quite different from the project total. WeatherServlet has no coverage from these tests, and it’s included in that total. So is Constants, whose initialization runs through the weather tests. I kept both in the report.
These columns count executed bytecode instructions and branch outcomes. They don’t grade the assertions. We checked one assertion separately by breaking the Paris mapping. Servlet requests and live HTTP failures still need their own tests, with the environment and connections isolated deliberately.
I’d keep this suite. It runs on the original Java 8 project, and it caught the mistake I put into the weather mapping. Before carrying it into the upgraded copy from part one, I’d run it there as well.
The part I’d work most diligent on for another application is the strategy review. I kept the existing framework, rewrote the strategy, and then had to narrow the candidate list separately. Once Bob had that brief, I could leave the Java it wrote alone.



