Compare objetos de fecha con diferentes niveles de precisión


81

Tengo una prueba de JUnit que falla porque los milisegundos son diferentes. En este caso, no me importan los milisegundos. ¿Cómo puedo cambiar la precisión de la aserción para ignorar milisegundos (o cualquier precisión que me gustaría establecer)?

Ejemplo de una afirmación fallida que me gustaría aprobar:

Date dateOne = new Date();
dateOne.setTime(61202516585000L);
Date dateTwo = new Date();
dateTwo.setTime(61202516585123L);
assertEquals(dateOne, dateTwo);

Respuestas:


22

Use un DateFormatobjeto con un formato que muestre solo las partes que desea hacer coincidir y haga una assertEquals()en las cadenas resultantes. También puede envolverlo fácilmente en su propio assertDatesAlmostEqual()método.


14
No maneja el caso de una diferencia de milisegundos sobre un segundo límite, 10.000 y 09.999 serían diferentes.
scarba05

61

Otra solución más, lo haría así:

assertTrue("Dates aren't close enough to each other!", (date2.getTime() - date1.getTime()) < 1000);

4
+1 para comparar la varianza, pero no tiene en cuenta la varianza absoluta (p. Ej., ¿Qué pasa si date1 es posterior a date2?)
Ophidian

13
Por lo general, adopto un enfoque similar simplemente envolviendo eso con Math.abs ()
parxier

59

Hay bibliotecas que ayudan con esto:

Apache commons-lang

Si tiene Apache commons-lang en su classpath, puede usarlo DateUtils.truncatepara truncar las fechas en algún campo.

assertEquals(DateUtils.truncate(date1,Calendar.SECOND),
             DateUtils.truncate(date2,Calendar.SECOND));

Hay una abreviatura para esto:

assertTrue(DateUtils.truncatedEquals(date1,date2,Calendar.SECOND));

Tenga en cuenta que 12: 00: 00.001 y 11: 59: 00.999 se truncarán a valores diferentes, por lo que esto podría no ser ideal. Para eso, hay ronda:

assertEquals(DateUtils.round(date1,Calendar.SECOND),
             DateUtils.round(date2,Calendar.SECOND));

AssertJ

A partir de la versión 3.7.0, AssertJ agregó isCloseToaserciones, si está utilizando la API de fecha / hora de Java 8.

LocalTime _07_10 = LocalTime.of(7, 10);
LocalTime _07_42 = LocalTime.of(7, 42);
assertThat(_07_10).isCloseTo(_07_42, within(1, ChronoUnit.HOURS));
assertThat(_07_10).isCloseTo(_07_42, within(32, ChronoUnit.MINUTES));

También funciona con fechas de Java heredadas:

Date d1 = new Date();
Date d2 = new Date();
assertThat(d1).isCloseTo(d2, within(100, ChronoUnit.MILLIS).getValue());

esta es la solución que estaba buscando :)
geoaxis

1
¡Gracias, esto me ahorró un montón de tiempo!
Robert Beltran

¿Por qué no usar DateUtils.round?
domi

1
Round también funcionaría. Se redondeará hacia arriba o hacia abajo, mientras que truncar siempre bajará. Según los documentos , round también maneja el horario de verano.
Dan Watt

Tuve el mismo problema java.sql.Timestampsy DateUtils.truncate(...)funcionó para mí en Java 8. Mi caso particular incluía una tecnología de base de datos que no permitía guardar un grano más fino que un segundo, por lo que estaba comparando una marca de tiempo en memoria con una que se había guardado y recuperado de una base de datos. La marca de tiempo en memoria tuvo mayor precisión que la marca de tiempo que se leyó de la base de datos.
Kent Bull

6

Podrías hacer algo como esto:

assertTrue((date1.getTime()/1000) == (date2.getTime()/1000));

No se necesitan comparaciones de cadenas.


Creo que quisiste decir "/" versus "%". Esto se vuelve complicado con respecto a la precisión arbitraria, en mi humilde opinión. Aunque es un buen punto.
Michael Easter

¡Ups! Buena atrapada. Sin embargo, no creo que la precisión sea un problema. Date.getTime () siempre devuelve un largo de ms desde la época.
Seth

1
Esto fallará si un valor es 3.999 segundos y el otro 4.000. En otras palabras, a veces tolerará una diferencia de hasta un segundo, a veces fallará por una diferencia de 2 ms.
David Balažic

6

En JUnit puedes programar dos métodos de aserción, como este:

public class MyTest {
  @Test
  public void test() {
    ...
    assertEqualDates(expectedDateObject, resultDate);

    // somewhat more confortable:
    assertEqualDates("01/01/2012", anotherResultDate);
  }

  private static final String DATE_PATTERN = "dd/MM/yyyy";

  private static void assertEqualDates(String expected, Date value) {
      DateFormat formatter = new SimpleDateFormat(DATE_PATTERN);
      String strValue = formatter.format(value);
      assertEquals(expected, strValue);
  }

  private static void assertEqualDates(Date expected, Date value) {
    DateFormat formatter = new SimpleDateFormat(DATE_PATTERN);
    String strExpected = formatter.format(expected);
    String strValue = formatter.format(value);
    assertEquals(strExpected, strValue);
  }
}

4

No sé si hay soporte en JUnit, pero una forma de hacerlo:

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

public class Example {

    private static SimpleDateFormat formatter = new SimpleDateFormat("dd MMM yyyy HH:mm:ss");

    private static boolean assertEqualDates(Date date1, Date date2) {
        String d1 = formatter.format(date1);            
        String d2 = formatter.format(date2);            
        return d1.equals(d2);
    }    

    public static void main(String[] args) {
        Date date1 = new Date();
        Date date2 = new Date();

        if (assertEqualDates(date1,date2)) { System.out.println("true!"); }
    }
}

Si llamas al método assertEqualDates, haré su tipo de retorno voidy haré la última línea assertEquals(d1, d2). De esta forma se comportaría igual que todos los assert*métodos JUnit .
Joachim Sauer

Convenido. Quería ejecutar el código y no tenía JUnit a mano.
Michael Easter

1
Tenga cuidado con los formateadores de fechas globales. No son seguros para subprocesos. No es un problema con este código, pero es un mal hábito.
itsadok

1
Esto no maneja el caso en el que los dos objetos Date tienen una diferencia inferior a un segundo pero cruzan el segundo umbral.
Ophidian

3

En realidad, este es un problema más difícil de lo que parece debido a los casos límite en los que la varianza que no le importa cruza un umbral para un valor que está verificando. por ejemplo, la diferencia de milisegundos es inferior a un segundo, pero las dos marcas de tiempo cruzan el segundo umbral, el umbral de los minutos o el umbral de la hora. Esto hace que cualquier enfoque de DateFormat sea inherentemente propenso a errores.

En cambio, sugeriría comparar las marcas de tiempo reales en milisegundos y proporcionar un delta de variación que indique lo que considera una diferencia aceptable entre los dos objetos de fecha. A continuación, se muestra un ejemplo demasiado detallado:

public static void assertDateSimilar(Date expected, Date actual, long allowableVariance)
{
    long variance = Math.abs(allowableVariance);

    long millis = expected.getTime();
    long lowerBound = millis - allowableVariance;
    long upperBound = millis + allowableVariance;

    DateFormat df = DateFormat.getDateTimeInstance();

    boolean within = lowerBound <= actual.getTime() && actual.getTime() <= upperBound;
    assertTrue(MessageFormat.format("Expected {0} with variance of {1} but received {2}", df.format(expected), allowableVariance, df.format(actual)), within);
}

2

Con JUnit 4 también puede implementar un comparador para probar fechas de acuerdo con la precisión elegida. En este ejemplo, el comparador toma una expresión de formato de cadena como parámetro. El código no es más corto para este ejemplo. Sin embargo, la clase de emparejamiento puede reutilizarse; y si le da un nombre descriptivo, puede documentar la intención con la prueba de una manera elegante.

import static org.junit.Assert.assertThat;
// further imports from org.junit. and org.hamcrest.

@Test
public void testAddEventsToBaby() {
    Date referenceDate = new Date();
    // Do something..
    Date testDate = new Date();

    //assertThat(referenceDate, equalTo(testDate)); // Test on equal could fail; it is a race condition
    assertThat(referenceDate, sameCalendarDay(testDate, "yyyy MM dd"));
}

public static Matcher<Date> sameCalendarDay(final Object testValue, final String dateFormat){

    final SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);

    return new BaseMatcher<Date>() {

        protected Object theTestValue = testValue;


        public boolean matches(Object theExpected) {
            return formatter.format(theExpected).equals(formatter.format(theTestValue));
        }

        public void describeTo(Description description) {
            description.appendText(theTestValue.toString());
        }
    };
}

2

use aserciones de AssertJ para Joda-Time ( http://joel-costigliola.github.io/assertj/assertj-joda-time.html )

import static org.assertj.jodatime.api.Assertions.assertThat;
import org.joda.time.DateTime;

assertThat(new DateTime(dateOne.getTime())).isEqualToIgnoringMillis(new DateTime(dateTwo.getTime()));

el mensaje de prueba fallida es más legible

java.lang.AssertionError: 
Expecting:
  <2014-07-28T08:00:00.000+08:00>
to have same year, month, day, hour, minute and second as:
  <2014-07-28T08:10:00.000+08:00>
but had not.

1
AssertJ también funciona para java.util.date:assertThat(new Date(2016 - 1900, 0, 1,12,13,14)).isEqualToIgnoringMillis("2016-01-01T12:13:14");
Dan Watt

1

Si estaba usando Joda, podría usar Fest Joda Time .


3
¿Podría proporcionar más información sobre cómo se debería implementar? De lo contrario, esto debería convertirse en un comentario.
Hugo Dozois

1

Simplemente compare las partes de la fecha que le interesan comparar:

Date dateOne = new Date();
dateOne.setTime(61202516585000L);
Date dateTwo = new Date();
dateTwo.setTime(61202516585123L);

assertEquals(dateOne.getMonth(), dateTwo.getMonth());
assertEquals(dateOne.getDate(), dateTwo.getDate());
assertEquals(dateOne.getYear(), dateTwo.getYear());

// alternative to testing with deprecated methods in Date class
Calendar calOne = Calendar.getInstance();
Calendar calTwo = Calendar.getInstance();
calOne.setTime(dateOne);
calTwo.setTime(dateTwo);

assertEquals(calOne.get(Calendar.MONTH), calTwo.get(Calendar.MONTH));
assertEquals(calOne.get(Calendar.DATE), calTwo.get(Calendar.DATE));
assertEquals(calOne.get(Calendar.YEAR), calTwo.get(Calendar.YEAR));

Me gusta mucho más este enfoque que usar un formateador de fechas. El único problema es que los campos getter específicos en Date están obsoletos. Es mejor usar un calendario para hacer lo mismo.
kfox

Ah, es bueno señalar que esos métodos están en desuso. Actualicé mi respuesta con el código alternativo para convertir y comparar objetos de Calendario en su lugar.
Oliver Hernandez

1

JUnit tiene una aserción incorporada para comparar dobles y especificar qué tan cerca deben estar. En este caso, el delta está dentro de los milisegundos que considera equivalentes a las fechas. Esta solución no tiene condiciones de límite, mide la varianza absoluta, puede especificar fácilmente la precisión y no requiere que se escriban bibliotecas o código adicional.

    Date dateOne = new Date();
    dateOne.setTime(61202516585000L);
    Date dateTwo = new Date();
    dateTwo.setTime(61202516585123L);
    // this line passes correctly 
    Assert.assertEquals(dateOne.getTime(), dateTwo.getTime(), 500.0);
    // this line fails correctly
    Assert.assertEquals(dateOne.getTime(), dateTwo.getTime(), 100.0);

Nota Debe ser 100.0 en lugar de 100 (o se necesita un lanzamiento para duplicar) para forzarlo a compararlos como dobles.


1

Puede elegir qué nivel de precisión desea al comparar fechas, por ejemplo:

LocalDateTime now = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS);
// e.g. in MySQL db "timestamp" is without fractional seconds precision (just up to seconds precision)
assertEquals(myTimestamp, now);

0

Algo como esto podría funcionar:

assertEquals(new SimpleDateFormat("dd MMM yyyy").format(dateOne),
                   new SimpleDateFormat("dd MMM yyyy").format(dateTwo));

0

En lugar de usarlo new Datedirectamente, puede crear un pequeño colaborador, que puede simular en su prueba:

public class DateBuilder {
    public java.util.Date now() {
        return new java.util.Date();
    }
}

Cree un miembro de DateBuilder y cambie las llamadas de new DateadateBuilder.now()

import java.util.Date;

public class Demo {

    DateBuilder dateBuilder = new DateBuilder();

    public void run() throws InterruptedException {
        Date dateOne = dateBuilder.now();
        Thread.sleep(10);
        Date dateTwo = dateBuilder.now();
        System.out.println("Dates are the same: " + dateOne.equals(dateTwo));
    }

    public static void main(String[] args) throws InterruptedException {
        new Demo().run();
    }
}

El método principal producirá:

Dates are the same: false

En la prueba, puede inyectar un talón de DateBuildery dejar que devuelva el valor que desee. Por ejemplo, con Mockito o una clase anónima que anula now():

public class DemoTest {

    @org.junit.Test
    public void testMockito() throws Exception {
        DateBuilder stub = org.mockito.Mockito.mock(DateBuilder.class);
        org.mockito.Mockito.when(stub.now()).thenReturn(new java.util.Date(42));

        Demo demo = new Demo();
        demo.dateBuilder = stub;
        demo.run();
    }

    @org.junit.Test
    public void testAnonymousClass() throws Exception {
        Demo demo = new Demo();
        demo.dateBuilder = new DateBuilder() {
            @Override
            public Date now() {
                return new Date(42);
            }
        };
        demo.run();
    }
}

0

Convierta las fechas en String usando SimpleDateFromat, especifique en el constructor los campos de fecha / hora requeridos y compare los valores de cadena:

SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String expectedDate = formatter.format(dateOne));
String dateToTest = formatter.format(dateTwo);
assertEquals(expectedDate, dateToTest);


0

Aquí hay una función de utilidad que hizo el trabajo por mí.

    private boolean isEqual(Date d1, Date d2){
        return d1.toLocalDate().equals(d2.toLocalDate());
    }


-1

lanzo los objetos a java.util.Date y comparo

assertEquals((Date)timestamp1,(Date)timestamp2);

Esto hará que la aserción falle debido a la precisión.
TechCrunch
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.