Kira Resari
Kira Resari

Reputation: 2430

How to make sure test always runs in the same time zone

I have a function that parses an Unix epoch time into the format yyyy-MM-dd'T'HH:mm:ss.SSSXXX like this in order to export it to a file:

    public static final SimpleDateFormat REQIF_DATE_FORMAT_WITH_MILLIS
        = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");

    public static String convertEpochStringToReqifDateString(String epochString) {
        Date timestamp = new Date(Long.parseLong(epochString));
        return REQIF_DATE_FORMAT_WITH_MILLIS.format(timestamp);
    }

Now, I have tests for this export, but while they pass locally, they fail on the server because it's apparently in a different time zone. Specifically, the differences look like this:

LAST-CHANGE="2017-03-13T21:36:44.261+01:00"
LAST-CHANGE="2017-03-13T20:36:44.261Z"

I have already tried running a number of things before the test in order to make sure that the test is always run in the same time zone, such as:

TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
System.setProperty("user.timezone", "UTC");

As well as the JUnitPioneer annotation:

@DefaultTimeZone("UTC")

...however, none of them seemed to affect the parsing output at all.

What can I do about this? All I want is some way to ensure that my tests are run in the same time zone regardless of where the machine they are run is standing so I can test the export correctly.

Upvotes: 2

Views: 2805

Answers (2)

Arvind Kumar Avinash
Arvind Kumar Avinash

Reputation: 79105

java.time

The java.util Date-Time API and their formatting API, SimpleDateFormat are outdated and error-prone. It is recommended to stop using them completely and switch to the modern Date-Time API*.

You can use Instant.ofEpochMilli to convert the epoch milliseconds into Instant and then use the Instant#toString. However, Instant#toString omits the fraction-of-second part if they are zero. Therefore, if you need the value strictly in the pattern, yyyy-MM-dd'T'HH:mm:ss.SSSXXX, you can convert the Instant to OffsetDateTime and format it using a DateTimeFormatter.

Solution using java.time, the modern Date-Time API:

import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

public class Main {
    public static void main(String[] args) {
        // An example epoch milliseconds
        long millis = 1631113620000L;

        Instant instant = Instant.ofEpochMilli(millis);
        String strDateTime = instant.toString();
        System.out.println(strDateTime);

        // If you need the value strictly in the pattern, yyyy-MM-dd'T'HH:mm:ss.SSSXXX
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ss.SSSXXX", Locale.ENGLISH);
        OffsetDateTime odt = instant.atOffset(ZoneOffset.UTC);
        strDateTime = odt.format(dtf);
        System.out.println(strDateTime);
    }
}

Output:

2021-09-08T15:07:00Z
2021-09-08T15:07:00.000Z

ONLINE DEMO

Learn more about the modern Date-Time API from Trail: Date Time.


* For any reason, if you have to stick to Java 6 or Java 7, you can use ThreeTen-Backport which backports most of the java.time functionality to Java 6 & 7. If you are working for an Android project and your Android API level is still not compliant with Java-8, check Java 8+ APIs available through desugaring and How to use ThreeTenABP in Android Project.

Upvotes: 6

Andy Turner
Andy Turner

Reputation: 140319

A better way to do this would be to use java.time.Instant:

Instant instant = Instant.ofEpochMilli(Long.parseLong(epochString));
return instant.toString();

This will always print in UTC, whatever the JVM's default time zone.


You can also do this by setting the time zone on the SimpleDateFormatter:

REQIF_DATE_FORMAT_WITH_MILLIS.setTimeZone(TimeZone.getTimeZone("UTC"));

Note that you need to take care with a shared SimpleDateFormat because it has mutable state, which is corrupted by access from multiple threads. You can have a separate instance per thread like so:

static final ThreadLocal<SimpleDateFormat> REQIF_DATE_FORMAT_WITH_MILLIS =
    ThreadLocal.withInitial(() -> {
      SimpleDateFormat sdf = new SimpleDateFormat();
      sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
      return sdf;
    });

and then access it with:

return REQIF_DATE_FORMAT_WITH_MILLIS.get().format(timestamp);

But this is getting quite messy, isn't it? Much easier just to use Instant.

Upvotes: 2

Related Questions