Tyvain
Tyvain

Reputation: 2760

Java Testing Calendar by changing current date

I have this fonction:

/**
 * @return From november to december -> current year +1 else current year
 */
public static int getCurrentScolYear () {
    int month = Calendar.getInstance().get(Calendar.MONTH);
    if (  month == NOVEMBER || month == DECEMBER) {
        return Calendar.getInstance().get(Calendar.YEAR) +1;
    }

    return Calendar.getInstance().get(Calendar.YEAR);
}

This is used to calculate a scolar year (that start in november).

I would like to test this in a Junit test, by changing the 'current time'. So I can test the result for different dates.

I find this thread but it seems over complicated: java: how to mock Calendar.getInstance()?

Is there a simple solution to test this?

Upvotes: 2

Views: 2271

Answers (1)

Basil Bourque
Basil Bourque

Reputation: 338835

java.time

The terrible Calendar class was supplanted years ago by the java.time classes, specifically ZonedDateTime. There is no reason to ever using Calendar again.

Below is my replacement for your code.

Custom class

We could return the type-safe and self-explanatory java.time.Year object rather than a mere integer number. But that would be misleading as that class is explicitly defined as an ISO 8601 compliant year number.

I suggest instead you define your own AcademicYear class. Pass around objects of this class to make your code more self-documenting, provide for type-safety, and ensure valid values. Something like the following class.

This custom class also provides a home for your static methods.

This class follows the lead of the java.time.Year class, including its naming conventions.

An academic year is actually two year numbers, the starting year and the following stopping year. The class below stores both.

Example usages:

AcademicYear.now().getDisplayName() // 2017-2018

or

AcademicYear.now().getValueStart() // First year of the two-year range, such as 2018 of the year 2018-2019.

I threw this class together without testing. So use this as a guide, not production code.

AcademicYear.java

package com.basilbourque.example;

import java.time.*;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;

// This class follows the lead of the `java.time.Year` class.
// See JavaDoc: https://docs.oracle.com/javase/10/docs/api/java/time/Year.html
// See OpenJDK source-code: http://hg.openjdk.java.net/jdk10/master/file/be620a591379/src/java.base/share/classes/java/time/Year.java
// This class follows the immutable objects pattern. So only getter accessor methods, no setters.
public class AcademicYear {
    // Statics
    static public int FIRST_YEAR_LIMIT = 2017;
    static public int FUTURE_YEARS_LIMIT = 10;
    static public Set < Month > academicYearStartingMonths = EnumSet.of( Month.NOVEMBER , Month.DECEMBER );

    // Members
    private int academicYearNumberStart;

    // Constructor
    private AcademicYear ( int academicYearNumberStart ) {
        if ( academicYearNumberStart < AcademicYear.FIRST_YEAR_LIMIT ) {
            throw new IllegalArgumentException( "Received a year number: " + academicYearNumberStart + " that is too long ago (before " + AcademicYear.FIRST_YEAR_LIMIT + "). Message # c5fd65c1-ed10-4fa1-96db-77d08ef1d97e." );
        }
        if ( academicYearNumberStart > Year.now().getValue() + AcademicYear.FUTURE_YEARS_LIMIT ) {
            throw new IllegalArgumentException( "Received a year number that is too far in the future, over " + AcademicYear.FUTURE_YEARS_LIMIT + " away. Message # 8581e4ca-afb3-4ab7-8849-9b02c434eb4c." );
        }
        this.academicYearNumberStart = academicYearNumberStart;
    }

    public static AcademicYear of ( int academicYearNumberStart ) {
        return new AcademicYear( academicYearNumberStart );
    }

    public int getValueStart ( ) {
        return this.academicYearNumberStart;
    }

    public int getValueStop ( ) {
        return ( this.academicYearNumberStart + 1 );
    }

    public String getDisplayName ( ) {
        String s = this.academicYearNumberStart + "-" + ( this.academicYearNumberStart + 1 );
        return s;
    }

    // ------| `Object`  |---------------------

    @Override
    public String toString ( ) {
        return "AcademicYear{ " +
                "academicYearNumberStart=" + academicYearNumberStart +
                " }";
    }

    @Override
    public boolean equals ( Object o ) {
        if ( this == o ) return true;
        if ( o == null || getClass() != o.getClass() ) return false;
        AcademicYear that = ( AcademicYear ) o;
        return this.getDisplayName().equals( that.getDisplayName() );
    }

    @Override
    public int hashCode ( ) {
        return Objects.hash( this.getDisplayName() );
    }

    // -----------|  Factory methods  |-------------------

    static public AcademicYear now ( ) {  // I think making ZoneId optional is a poor design choice, but I do so here to mimic `java.time.Year`.
        AcademicYear ay = AcademicYear.now( Clock.systemDefaultZone() );
        return ay;
    }

    static public AcademicYear now ( ZoneId zoneId ) {
        // Determine the current date as seen in the wall-clock time used by the people of a particular region (a time zone).
        AcademicYear ay = AcademicYear.now( Clock.system( zoneId ) );
        return ay;
    }

    static public AcademicYear now ( Clock clock ) {
        final LocalDate localDate = LocalDate.now( clock );
        AcademicYear ay = AcademicYear.from( localDate );
        return ay;
    }

    static public AcademicYear from ( LocalDate localDate ) {
        Objects.requireNonNull( localDate , "Received a null `LocalDate` object. Message # 558dd5e8-5cff-4c6e-b0f8-40dbcd76a753." );
        // Extract the month of the specified date. If not Nov or Dec, subtract one from the year.
        int y = localDate.getYear();
        // If not November or December, subtract 1.
        int startingYear;
        if ( ! academicYearStartingMonths.contains( localDate.getMonth() ) ) {
            startingYear = ( y - 1 );
        } else {
            startingYear = y;
        }
        AcademicYear ay = AcademicYear.of( startingYear );
        return ay;
    }

}

ZoneId

Another change is the required argument for ZoneId, a time zone.

A time zone is crucial in determining a date, and therefore a year. For any given moment, the date varies around the globe by zone. For example, a few minutes after midnight in Paris France is a new day while still “yesterday” in Montréal Québec.

If no time zone is specified, the JVM implicitly applies its current default time zone. That default may change at any moment during runtime(!), so your results may vary. Better to specify your desired/expected time zone explicitly as an argument.

Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 2-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).

Break up methods

Another change, is we have two sets of methods rather than one single method.

One method, AcademicYear.from, has responsibility for the logic of making the November adjustment. You pass a LocalDate to this method.

The other methods, three variations of AcademicYear.now, are responsible for determining the current moment, adjusting to the specified time zone, and extracting a date-only LocalDate object. That LocalDate object is then passed to the other method.

Clock for unit-testing

For testing, you can pass to one of the now method variations a Clock instance that alters time as you may desire. The Clock class provides several clocks with altered behavior, such as fixed-point in time, current moment plus/minus a span-of-time, and altered cadence where the clock ticks in your specified granularity such as every 5 minutes.

For more info on the altered Clock behaviors, see my Answer to another Question.

Example:

ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
LocalDate ld = LocalDate.of( 2018 , Month.JANUARY , 23 ) ;  // 2018-01-23
LocalTime lt = LocalTime.of( 8 , 0 ) ; // 8 AM.
Instant instant = ZonedDateTime.of( ld , lt , z ).toInstant() ;
Clock c = Clock.fixed( instant , z ) ;
AcademicYear ay = AcademicYear.now( c ) ;

About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Upvotes: 5

Related Questions