| name | Write Holiday Tests |
| description | Write country-specific holiday tests using CalendarCheckerApi |
How to Write Holiday Tests
This guide explains how to write tests for new or modified holiday calendars using the CalendarCheckerApi.
File Location
Create test file at: jollyday-tests/src/test/java/de/focus_shift/jollyday/tests/[CountryName]Test.java
Example naming:
- Germany:
HolidayDETest.java
- United States:
HolidayUSTest.java
- France:
HolidayFRTest.java
CalendarCheckerApi
The CalendarCheckerApi provides fluent assertions for testing holidays:
import static de.focus_shift.jollyday.tests.CalendarCheckerApi.assertFor;
import static de.focus_shift.jollyday.core.HolidayCalendar.GERMANY;
import static java.time.Month.JANUARY;
import static java.time.Month.FEBRUARY;
Basic Usage
@Test
void ensuresHolidays() {
assertFor(GERMANY)
.hasFixedHoliday("NEW_YEAR", JANUARY, 1).and()
.hasChristianHoliday("GOOD_FRIDAY").and()
.hasIslamicHoliday("ID_UL_ADHA")
.check();
}
Assertion Methods
Fixed Date Holidays
.hasFixedHoliday("NEW_YEAR", JANUARY, 1)
.hasFixedHoliday("JUNETEENTH", JUNE, 19, 2021)
.hasFixedHoliday("REFORMATION_DAY", OCTOBER, 31, 2017, null)
Christian (Easter-based) Holidays
.hasChristianHoliday("GOOD_FRIDAY")
.hasChristianHoliday("EASTER_MONDAY")
.hasChristianHoliday("ASCENSION_DAY")
.hasChristianHoliday("WHIT_MONDAY")
.hasChristianHoliday("CORPUS_CHRISTI")
Islamic Calendar Holidays
.hasIslamicHoliday("ID_AL_FITR")
.hasIslamicHoliday("ID_UL_ADHA")
.hasIslamicHoliday("NEWYEAR")
.hasIslamicHoliday("HARI_RAYA_PUASA", true)
Fixed Weekday Holidays
The XSD defines three distinct "weekday-based" holiday types. Each has its own assertion method,
which independently recomputes the expected date rather than calling into the production
calculator — this keeps the check a genuine verification of the XML config, not a tautology
against the same code being tested.
.hasFixedWeekdayHoliday("THANKSGIVING", FOURTH, THURSDAY, NOVEMBER)
.validFrom(Year.of(1863))
.hasFixedWeekdayBetweenFixedHoliday("HUSBANDS_DAY", FRIDAY, MonthDay.of(JANUARY, 19), MonthDay.of(JANUARY, 25))
.hasFixedWeekdayRelativeToFixedHoliday("FIRST_DAY_SUMMER", FIRST, THURSDAY, AFTER, MonthDay.of(APRIL, 18))
Occurrence (FIRST/SECOND/THIRD/FOURTH/LAST) and Relation (BEFORE/AFTER/CLOSEST) come from
de.focus_shift.jollyday.core.spi. Note: Occurrence.LAST is not supported for
hasFixedWeekdayRelativeToFixedHoliday (throws UnsupportedOperationException) — production's
day-offset calculation has no defined behavior for LAST in that specific holiday type.
A fourth, related type has its own method:
.hasRelativeToWeekdayInMonthHoliday("SERVICE_REDUCTION", FRIDAY, BEFORE, LAST, MONDAY, MAY, OBSERVANCE)
.inSubdivision("md")
Relation.CLOSEST is not supported for hasRelativeToWeekdayInMonthHoliday (throws
UnsupportedOperationException) — production's RelativeToWeekdayInMonthParser only branches on
BEFORE vs. everything-else, so CLOSEST silently behaves like AFTER rather than computing an
actual closest-day distance; no shipped config currently relies on that behavior.
Periodic Holidays (every attribute)
Any holiday-bearing XML element can carry an every="..." attribute (Limited.YearCycle), restricting
it to a subset of years within its valid range. .every(cycle) handles the two cycles that need no
anchor; .every(cycle, referenceYear) handles the rest, anchored at the year matching the XML's
validFrom (or validTo if there's no validFrom) — calling the wrong overload for a given cycle
throws IllegalArgumentException immediately, rather than silently misbehaving:
.hasFixedHoliday("ASSUMPTION_BLESSED_VIRGIN_MARY", AUGUST, 15)
.every(EVEN_YEARS)
.validBetween(YEAR_FROM, YEAR_TO)
.hasFixedHoliday("SOME_HOLIDAY", MARCH, 1)
.every(FOUR_YEARS, Year.of(2014))
.validBetween(Year.of(2014), Year.of(2030))
For a validBetween/validFrom/validTo range with a cycle set, years that don't match the cycle are
asserted absent (not skipped) — the check is exhaustive, not just a presence spot-check. Because of
this, be careful with overlapping XML entries that combine to a broader pattern than any single entry's
own cycle: if several XML entries for the same key have different (possibly complementary) cycles across
overlapping year ranges, don't transcribe them 1:1 — instead determine the actual combined observable
behavior (e.g. by probing the real HolidayManager output across the affected years) and assert that,
since production doesn't distinguish between the underlying XML entries, only the resulting holiday
presence per year.
Regional/Subdivision Holidays
.hasFixedHoliday("REPELLENCE_PRAYER", NOVEMBER, 20)
.inSubdivision("bw")
.check();
.hasFixedHoliday("EPIPHANY", JANUARY, 6)
.inSubdivision("by")
.and()
.hasFixedHoliday("EPIPHANY", JANUARY, 6)
.inSubdivision("bw")
.check();
Complete Test Example
package de.focus_shift.jollyday.tests;
import de.focus_shift.jollyday.core.HolidayCalendar;
import org.junit.jupiter.api.Test;
import static de.focus_shift.jollyday.tests.CalendarCheckerApi.assertFor;
import static java.time.Month.*;
class HolidayGETest {
@Test
void ensuresHolidays() {
assertFor(HolidayCalendar.GERMANY)
.hasFixedHoliday("NEW_YEAR", JANUARY, 1).and()
.hasFixedHoliday("LABOUR_DAY", MAY, 1).and()
.hasFixedHoliday("CHRISTMAS", DECEMBER, 25).and()
.hasFixedHoliday("SECOND_CHRISTMAS_DAY", DECEMBER, 26).and()
.hasFixedHoliday("EPIPHANY", JANUARY, 6)
.inSubdivision("by")
.and()
.hasFixedHoliday("ASCENSION_DAY", MAY)
.inSubdivision("by")
.and()
.hasFixedHoliday("EPIPHANY", JANUARY, 6)
.inSubdivision("bw")
.and()
.hasFixedHoliday("REPELLENCE_PRAYER", NOVEMBER)
.inSubdivision("bw")
.and()
.hasChristianHoliday("GOOD_FRIDAY").and()
.hasChristianHoliday("EASTER_MONDAY").and()
.hasChristianHoliday("ASCENSION_DAY").and()
.hasChristianHoliday("WHIT_MONDAY")
.check();
}
}
Testing Edge Cases
Moving Conditions (Weekend Shift)
@Test
void ensuresMovingCondition() {
assertFor(HolidayCalendar.US)
.hasFixedHoliday("NEW_YEAR", JANUARY, 1).canBeMovedFrom(SATURDAY, FRIDAY)
.check();
}
Historical Changes
@Test
void ensuresHistoricalChanges() {
assertFor(HolidayCalendar.GERMANY)
.hasFixedHoliday("REFORMATION", OCTOBER, 31)
.validBetween(Year.of(2017), Year.of(2017))
.check();
}
Best Practices
- Comprehensive coverage: Test all holidays including regional ones
- Valid ranges: Test holidays with
validFrom/validTo constraints
- Edge cases: Test years where holidays fall on weekends
- Clear naming: Use descriptive test method names
- Group related tests: Use
.and() to chain related assertions but not at the end of the chain before .check()
- Year ranges for Islamic holiday calendars: When a calendar includes at least one Islamic holiday, in test files, define
YEAR_FROM = 1900 and YEAR_TO = 2173 constants, and use .validBetween(Year.of(YEAR_FROM), Year.of(YEAR_TO)) for all holiday assertions since the entire calendar's valid range is constrained by these attributes.