You are using an Outdated Browser. For a better experience, keep your browser upto date. Check here for latest versions.

Manager's tests fail in Windows - Time Zones (Part 2)

linux / windows / Datetime / timezones / JAVA / .net / dotnet

Previously, we had an AbstractTestBase class that controlled the end of line (EOL) characters and time zone of all the tests extending from this class.

In an effort to remove the AbstractTestBase class and correct the unit tests that are sensitive to EOL characters, new tests failed, this time due to manipulation of the time zone.

As I mentioned in the previous part, the Java application is complementary to the VB6 application. For this, the application offers some REST resources. When executed in Windows, some of the tests for these entry points fail without explicit control of the time zone for Lisbon.

The following test is a very simplified version that passes in Linux:

import static org.junit.Assert.assertEquals;

import java.io.StringWriter;
import java.util.Date;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.stream.XMLStreamWriter;

import org.joda.time.LocalDate;
import org.junit.Test;

public class TimeZoneTest {

	@XmlRootElement
	public static class GetCitizenCardResponse {
		
		private Date birthDate;

		public Date getBirthDate() {
			return birthDate;
		}
		public void setBirthDate(Date birthDate) {
			this.birthDate = birthDate;
		}
	}
	
	@Test
	public void testDate() throws Exception {

		GetCitizenCardResponse response = new GetCitizenCardResponse();
		response.setBirthDate(new LocalDate(1975, 12, 1).toDate());

		JAXBContext jaxbContext = JAXBContext.newInstance(GetCitizenCardResponse.class);
		Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

		StringWriter sw = new StringWriter();

		jaxbMarshaller.marshal(response, sw);
		assertEquals(
				"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + 
				"<getCitizenCardResponse>" + 
					"<birthDate>1975-12-01T00:00:00+01:00</birthDate>" + 
				"</getCitizenCardResponse>",
				sw.toString());
	}
}

But it fails in Windows with:

<failure message="expected:&lt;...&gt;1975-12-01T00:00:00[+01:00]&lt;/birthDate&gt;&lt;/getCit...&gt; but was:&lt;...&gt;1975-12-01T00:00:00[Z]&lt;/birthDate&gt;&lt;/getCit...&gt;" type="org.junit.ComparisonFailure">

Further reducing the test, we get the following, which, like the previous one, passes in Linux but fails in Windows, where TimeZone.getDefault () first returns “Europe/London”:

public class TimeZoneTest {

	@Test
	public void testID() {
		assertEquals("Europe/Lisbon", TimeZone.getDefault().getID());
	}	
}

Confirming that both systems are in fact set for Lisbon is worthwhile:

In Linux, you can check with timedatectl, for example:

$ timedatectl|grep zone 
                       Time zone: Europe/Lisbon (WEST, +0100)

And in Windows, we have the following settings:

After some perplexity, because throughout my lifetime Lisbon and London have always been in the same time zone, it occurred to me that the year 1975 was important. If we change it to a more recent year, like 2000, the test passes in Windows successfully. In fact, in 1975, the legal time in Portugal was “60 minutes ahead of Greenwich Mean Time”. The following test proves this and runs successfully in Linux and Windows:

import static org.junit.Assert.*;

import java.time.LocalDateTime;
import java.time.ZoneId;

import org.junit.Test;

public class TimeZoneDiffTest {

    @Test
    public void testJava() throws Exception {
    
   	 ZoneId lisbon = ZoneId.of("Europe/Lisbon");
   	 ZoneId london = ZoneId.of("Europe/London");
   	 
   	 LocalDateTime d1 = LocalDateTime.of(1975, 12, 1, 0, 0);
   	 LocalDateTime d2 = LocalDateTime.of(2000, 12, 1, 0, 0);

   	 assertFalse(d1.atZone(lisbon).isEqual(d1.atZone(london)));
   	 assertTrue(d2.atZone(lisbon).isEqual(d2.atZone(london)));
    }
}

When we force the time zone and ignore system default settings, the Java application thus behaves as expected on both platforms. So, the problem is due to Java in Windows detecting London as my time zone while I am in Lisbon. This is not a problem in 2020, but it ended up being so for 1975.

As seen in the image above, Windows cannot distinguish between the time zones of Dublin, Edinburgh, Lisbon, and London, so Java has to implement an algorithm to choose only one of the four time zones.

This problem was solved in Java 12, which uses the mapping maintained by Unicode CLDR between Windows and the IANA (also known as Olson) system, which detects Lisbon as my time zone.

This problem also affects .NET technology, wherein the following program:

using System;

class Program
{
    static void Main(string[] args)
    {
        System.Console.WriteLine(TimeZoneInfo.Local.Id);
    }
}

In Linux returns:

$ dotnet run
Europe/Lisbon

In Windows:

$ dotnet run
GMT Standard Time

After realizing why the test in Windows fails, the question is “What now?”

We could force the application to run only in Java 12 or higher, but it is a very remote possibility for this project, because in addition to supporting Visual Basic 6, we still have a significant number of customers with Windows XP.

We could run only unit tests with Java 12 and Java 8 on customers, but it doesn't seem like a good idea or good practice to divert from what is executed in production.

Another option would be to use a library like time4j, which would behave like Java 12 in Java 8.

A fourth option would be to change the REST API so that the problem does not occur at all. The field that is returned in the API corresponds to a date of birth, where the time of birth is meaningless. The format used is ISO-8601, and a possible alternative to use Unix Time suffers from the same problem of excessive precision.

Perhaps it is ideal to follow law No. 5 “Don’t use time if you don’t need it” using an XmlAdapter:

import static org.junit.Assert.assertEquals;

import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import org.joda.time.LocalDate;
import org.junit.Test;

public class TimeZoneDateOnlyTest {

    public static class DateAdapter extends XmlAdapter<String, Date> {
    
    	private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    
    	@Override
    	public String marshal(Date v) throws Exception {
        	synchronized (dateFormat) {
            	return dateFormat.format(v);
        	}
    	}
    
    	@Override
    	public Date unmarshal(String v) throws Exception {
        	synchronized (dateFormat) {
            	return dateFormat.parse(v);
        	}
    	}
    
    }

    @XmlRootElement
    public static class GetCitizenCardResponse {
   	 
   	 private Date birthDate;

   	 @XmlElement(name = "birthDate", required = true)
   	 @XmlJavaTypeAdapter(DateAdapter.class)
   	 public Date getBirthDate() {
   		 return birthDate;
   	 }
   	 public void setBirthDate(Date birthDate) {
   		 this.birthDate = birthDate;
   	 }
    }
    
    @Test
    public void testJavaDate() throws Exception {
   	 GetCitizenCardResponse response = new GetCitizenCardResponse();
   	 response.setBirthDate(new LocalDate(1975, 12, 1).toDate());

   	 JAXBContext jaxbContext = JAXBContext.newInstance(GetCitizenCardResponse.class);
   	 Marshaller jaxbMarshaller = jaxbContext.createMarshaller();

   	 StringWriter sw = new StringWriter();

   	 jaxbMarshaller.marshal(response, sw);
   	 assertEquals(
   			 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" +
   			 "<getCitizenCardResponse>" +
   				 "<birthDate>1975-12-01</birthDate>" +
   			 "</getCitizenCardResponse>",
   			 sw.toString());
    }
}

Since the only customer of this REST API is under my control, it is capable of being a relatively easy change to implement.

However, for this specific case, none of the options discussed are the best, because this API was not even used. Thus, the best solution is to remove the feature completely.

I am curious to see whether or not this topic will come up again in the near future with the European Union’s proposal to end summer-time clock changes and the United Kingdom's Brexit from the European Union, whether or not Portugal and the United Kingdom will decide different time zones.

__________________
Featured image from:
https://static.globalnoticias.pt/dn/image.aspx?brand=DN&type=generate&guid=62aaa465-c552-4809-8aea-14a469b4af9b

comments powered by Disqus