Table of Contents
- Table of Contents
- Introduction
- TDD Cycle 1 – Time of Birth Default Value
- TDD Cycle 2 – Time of Birth Setter / Getter
- TDD Cycle 3 – GetAge – Part 1
- TDD Cycle 4 – GetAge – Part 2
- Commit
Introduction
In the last post, we created a simple enum for AgeUnits. Now, let’s consume that enum in our runtime code as we calculate the age of an animal.
The new switch expressions require an exhaustive listing of cases. We must specify what should be done with all possible values in the switch. The older switch statements would allow some, but not all of the cases to be covered.
TDD Cycle 1 – Time of Birth Default Value
Unit Test
Let’s start by testing the default value we expect when the No-Argument Constructor is called.
In src/test/java/codingchica.java101.model.AnimalTest.java, let’s add logic to test this new field via a getter:
/**
* Unit tests for the timeOfBirth field's getters/setters.
* @see Animal#getTimeOfBirth()
*/
@Nested
class TimeOfBirthTest {
@Test
void getTimeOfBirth_whenDefaultedInConstructor_thenReturnsExpectedValue() {
// Setup
Animal animal = new Animal();
// Execution
Instant result = animal.getTimeOfBirth();
// Validation
assertNull(result);
}
}
This code won’t compile, as we don’t yet have the new getter method to invoke. However, you are welcome to hit the Run (green arrow in IntelliJ) to confirm.
Runtime Code
Next, we should add the new field and the getter to src/main/java/codingchica.java101.model.Animal.java. We are going to use Lombok to generate the getter:
/**
* The date and time at which the animal was born or hatched.
*/
@Getter
private Instant timeOfBirth = null;
Maven Build Success
At this point, our Maven build should succeed. Let’s confirm before moving onto the next TDD cycle.
TDD Cycle 2 – Time of Birth Setter / Getter
We are going to be testing an age, which is based upon relative differences to the current date/time. Therefore, let’s create an enum that allows our tests to create relative date/time values (AKA Java Instant objects).
Unit Test – RelativeTime
Add a new file at src/test/java/codingchica.java101.model.RelativeTime.java. This time, we are going to need a constructor, as our enum values will also contain data and calculate additional data.
package codingchica.java101.model;
import lombok.Getter;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeParseException;
/**
* Relative times to use during testing.
*/
public enum RelativeTime {
/**
* Approximately ten years in the past.
*/
TEN_YEARS_AGO("-P3750D"),
/**
* Approximately one year in the past.
*/
YEAR_AGO("-P366D"),
/**
* Approximately one day in the past.
*/
DAY_AGO("-P1D"),
/**
* Approximately one hour in the past.
*/
HOUR_AGO("-PT1H10M"),
/**
* Approximately one hour in the future.
*/
HOUR_AHEAD("PT1H10M"),
/**
* Approximately one day in the future.
*/
DAY_AHEAD("P1DT5H"),
/**
* Approximately one year in the future.
*/
YEAR_AHEAD("P366D"),
/**
* Approximately ten years in the future.
*/
TEN_YEARS_AHEAD("P3750DT1H");
/**
* The date / time value to represent this scenario.
*/
@Getter
private final Instant instant;
@Getter
private final String durationString;
/**
* Private constructor for the enum objects.
* @param durationStringValue The string representing the desired duration.
*/
RelativeTime(String durationStringValue){
durationString = durationStringValue;
Duration durationValue;
try {
durationValue = Duration.parse(durationString);
} catch (DateTimeParseException e){
System.out.printf("Unable to parse duration value: %s%n", durationString);
throw e;
}
Instant instantValue = Instant.now();
instant = instantValue.plus(durationValue);
}
}
In this case, the values are each defined with the parameters that will be used to call the constructor, such as:
TEN_YEARS_AGO("-P3750D"),
That matches with the constructor that is defined at the bottom of the file:
RelativeTime(String durationStringValue){
We don’t need to tell Java that the constructor is private, that is expected in an enum. All of the objects that should be created are defined directly within this file. The logic within the constructor is just to setup the durationString and instant fields for that enum value. We change the relative time range from a String to the java.time.Duration that can be used to add to/take away from the current time (AKA Instant.now()).
You may also notice that there is a try/catch block in the constructor above. When initially running the AnimalTest that consumed this RelativeTime enum, the Constructor call failed with a DateTimeParseException, but I had difficulty troubleshooting because the exception thrown didn’t include which of the String values caused the exception. It happened during test initialization, so it wasn’t something I could log in the unit test itself. Therefore, I added a try/catch block and some additional troubleshooting output before re-throwing the original exception. Ideally, this would be using a logger, rather than System.out, but we haven’t talked about that yet. Well written code is responsible for not only producing the desired result, but also providing meaningful output to log files for troubleshooting.
Animal Test – Time of Birth Getter/Setter
Then, update the src/test/java/codingchica.java101.model.AnimalTest.java with expectations for a new field with both a getter and setter and using our new RelativeTime enum.
/**
* Unit tests for the timeOfBirth field's getters/setters.
* @see Animal#getTimeOfBirth()
* @see Animal#setTimeOfBirth(Instant)
*/
@Nested
class TimeOfBirthTest {
...
@ParameterizedTest
@EnumSource(value = RelativeTime.class, mode = EnumSource.Mode.EXCLUDE, names = {})
void getName_whenPopulatedFromSetter_thenReturnsExpectedValue(RelativeTime value) {
// Setup
Instant expectedValue = value.getInstant();
Animal animal = new Animal();
animal.setTimeOfBirth(expectedValue);
// Execution
Instant result = animal.getTimeOfBirth();
// Validation
assertSame(expectedValue, result);
}
}
In the example above, we use a parameterized test with an EnumSource. The mode = EXCLUDE tells JUnit to include all of the values within the enum, unless we list them in the names list.
Runtime Code Changes – Time Of Birth Getter and Setter
Next, let’s update the src/main/java/codingchica.java101.model.Animal.java code to also include the setter:
/**
* The date and time at which the animal was born or hatched.
*/
@Getter
@Setter
private Instant timeOfBirth = null;
Maven Build Success
If we run the Maven build now, it should show a successful build.
TDD Cycle 3 – GetAge – Part 1
Unit Test
Let’s do another test driven development (TDD) loop for the first part of the Animal updates.
AnimalTest – Get Age Default
In the case that birthDateTime is null (not set to an Object reference), we need to return a default value. Let’s update src/test/java/codingchica.java101.model.AnimalTest.java:
/**
* Unit tests for the getAge method.
* @see Animal#getAge(AgeUnits)
*/
@Nested
class GetAgeTest {
@Test
void getAge_whenNull_thenReturnsExpectedValue(){
// Setup
Animal animal = new Animal();
long expectedResult = -1;
// Execution
long result = animal.getAge(AgeUnits.YEARS);
// Validation
assertEquals(expectedResult, result);
}
}
Runtime Changes – Animal’s Get Age Method – Hard-Coded Response
Now that our unit tests are in place, let’s add the runtime logic for the new getAge method in src/main/java/codingchica.java101.model.Animal.java, but just enough to make this test pass, let’s add:
/**
* Ignoring leap year, how many days are in most years.
*/
private static final int AVERAGE_DAYS_IN_YEAR = 365;
/**
* Retrieve the age for the animal. An animal that has not yet been born
* will have a negative age.
*
* @param unit The unit of time in which the age returned should be
* measured.
* @return The age of the animal in the units provided, or a negative
* number if the animal has not yet been born or hatched.
*/
public long getAge(final AgeUnits unit) {
long age = -1;
return age;
}
I split the return statement apart from the definition of the age variable, as I know we’re going to circle back to this method in the next TDD loop.
Maven Build Success
Let’s run the Maven to ensure success before the next TDD loop.
TDD Cycle 4 – GetAge – Part 2
Unit Test
Now, we can loop again to add the logic to calculate the age when the timeOfBirth is available.
AnimalTest – Get Age Expectations
If adding logic via if-statements, we could add these tests in separate loops. However, I want to show how the new switch expression would work using the ApiUnits enum we created in the last post. If you want to do this in multiple TDD loops, you could start with the if-statement approach and change to the switch expression approach once all values are defined. Then, simply re-running the existing test cases that were added in each of the prior loops would be what you need for the refactoring validations. This is one of the beauties of TDD. By having good unit test code coverage, we can more freely refactor if we want to change the code within our method.
/**
* Unit tests for the getAge method.
* @see Animal#getAge(AgeUnits)
*/
@Nested
class GetAgeTest {
...
@ParameterizedTest
@EnumSource(value = RelativeTime.class, mode = EnumSource.Mode.EXCLUDE, names = {})
void getAge_whenYears_thenReturnsExpectedValue(RelativeTime value){
// Setup
Animal animal = new Animal();
animal.setTimeOfBirth(value.getInstant());
long expectedResult = switch (value){
case TEN_YEARS_AGO -> -10;
case YEAR_AGO -> -1;
case DAY_AGO,HOUR_AGO, HOUR_AHEAD, DAY_AHEAD -> 0;
case YEAR_AHEAD -> 1;
case TEN_YEARS_AHEAD -> 10;
};
// Execution
long result = animal.getAge(AgeUnits.YEARS);
// Validation
assertEquals(expectedResult, result, () -> String.format("%s: %s", value, value.getDurationString()));
}
@ParameterizedTest
@EnumSource(value = RelativeTime.class, mode = EnumSource.Mode.EXCLUDE, names = {})
void getAge_whenDays_thenReturnsExpectedValue(RelativeTime value){
// Setup
Animal animal = new Animal();
animal.setTimeOfBirth(value.getInstant());
long expectedResult = switch (value){
case TEN_YEARS_AGO -> -3750;
case YEAR_AGO -> -366;
case DAY_AGO -> -1;
case HOUR_AGO, HOUR_AHEAD -> 0;
case DAY_AHEAD -> 1;
case YEAR_AHEAD -> 365;
case TEN_YEARS_AHEAD -> 3750;
};
// Execution
long result = animal.getAge(AgeUnits.DAYS);
// Validation
assertEquals(expectedResult, result, () -> String.format("%s: %s", value, value.getDurationString()));
}
@ParameterizedTest
@EnumSource(value = RelativeTime.class, mode = EnumSource.Mode.EXCLUDE, names = {})
void getAge_whenHours_thenReturnsExpectedValue(RelativeTime value){
// Setup
Animal animal = new Animal();
animal.setTimeOfBirth(value.getInstant());
long expectedResult = switch (value){
case TEN_YEARS_AGO -> -90000;
case YEAR_AGO -> -8784;
case DAY_AGO -> -24;
case HOUR_AGO -> -1;
case HOUR_AHEAD -> 1;
case DAY_AHEAD -> 28;
case YEAR_AHEAD -> 8783;
case TEN_YEARS_AHEAD -> 90000;
};
// Execution
long result = animal.getAge(AgeUnits.HOURS);
// Validation
assertEquals(expectedResult, result, () -> String.format("%s: %s", value, value.getDurationString()));
}
}
The code above also uses a new format for the switch expression that we haven’t seen yet – one where the enum values are what is used for the comparison. With this approach, Java will help us enforce that we cover all of the scenarios.
Something else of note, is the variation between a single value, like:
case TEN_YEARS_AGO -> -10;
and a case that might match against multiple values, like:
case DAY_AGO, HOUR_AGO, HOUR_AHEAD, DAY_AHEAD -> 0;
Since we are using the newer switch expression, we need not worry about break statements. The semi-colon is what ends the logic (value selection) for each case.
Runtime Changes – Animal’s Get Age Method
Now that our unit tests are in place, let’s add the runtime logic for the new getAge method in src/main/java/codingchica.java101.model.Animal.java, so it isn’t just hard-coded:
/**
* Ignoring leap year, how many days are in most years.
*/
private static final int AVERAGE_DAYS_IN_YEAR = 365;
/**
* Retrieve the age for the animal. An animal that has not yet been born
* will have a negative age.
*
* @param unit The unit of time in which the age returned should be
* measured.
* @return The age of the animal in the units provided, or a negative
* number if the animal has not yet been born or hatched.
*/
public long getAge(final AgeUnits unit) {
long age = -1;
if (timeOfBirth != null) {
Instant now = Instant.now();
Duration difference = Duration.between(now, timeOfBirth);
age = switch (unit) {
case YEARS -> difference.toDays() / AVERAGE_DAYS_IN_YEAR;
case DAYS -> difference.toDays();
case HOURS -> difference.toHours();
};
}
return age;
}
The null check (age not being assigned to an Object reference) allows us to maintain the default value of -1 for the not-yet-hatched scenario. If we do have a timeOfBirth value to compare to the current time, we use the java.time.Duration class to retrieve the difference between the two points in time.
Here, we are also using the new switch expression style to define the value of age and the comparison is based upon the AgeUnits enum we created in the last post.
You may notice that we are also defining a new static final int called AVERAGE_DAYS_IN_YEAR. This isn’t perfect, leap year isn’t accounted for, but we have it externalized as a separate field, so we do not have a magic number in our calculations. Magic numbers are when we have a calculation that includes a hard-coded number value that isn’t 0 or 1, nor is it pulled from a variable with a possible comment as to how that number was determined. Magic numbers make code brittle and hard to maintain, as they carry no explanations and may be used in multiple locations. Instead, we have pulled this one out into a field with a meaningful name.
Maven Build Success
Let’s run the Maven build now to confirm that unit tests and quality gates (build breakers) are successful.
Commit
In an attempt to keep commits small, let’s commit our changes at this point.

Leave a comment