Vergleichen Sie Datumsobjekte mit unterschiedlicher Genauigkeit


81

Ich habe einen JUnit-Test, der fehlschlägt, weil die Millisekunden unterschiedlich sind. In diesem Fall interessieren mich die Millisekunden nicht. Wie kann ich die Genauigkeit der Zusicherung ändern, um Millisekunden zu ignorieren (oder eine Genauigkeit, auf die ich sie einstellen möchte)?

Beispiel für eine fehlerhafte Behauptung, die ich bestehen möchte:

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

Antworten:


22

Verwenden Sie ein DateFormatObjekt mit einem Format, das nur die Teile anzeigt, mit denen Sie übereinstimmen möchten, und führen Sie eine assertEquals()an den resultierenden Zeichenfolgen aus. Sie können dies auch einfach in Ihre eigene assertDatesAlmostEqual()Methode einwickeln .


14
Behandelt den Fall einer Millisekunden-Differenz über eine zweite Grenze nicht, wären 10.000 und 09.999 unterschiedlich.
scarba05

61

Noch eine Problemumgehung, ich würde es so machen:

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

4
+1 für den Vergleich der Varianz, berücksichtigt aber nicht die absolute Varianz (z. B. was ist, wenn Datum1 nach Datum2 liegt?)
Ophidian

13
Normalerweise verfolge ich einen ähnlichen Ansatz, indem ich ihn mit Math.abs ()
parxier

60

Es gibt Bibliotheken, die dabei helfen:

Apache commons-lang

Wenn Sie Apache commons-lang in Ihrem Klassenpfad haben, können Sie DateUtils.truncatedie Daten auf ein Feld kürzen.

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

Dafür gibt es eine Abkürzung:

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

Beachten Sie, dass 12: 00: 00.001 und 11: 59: 00.999 auf unterschiedliche Werte abgeschnitten werden, sodass dies möglicherweise nicht ideal ist. Dafür gibt es rund:

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

AssertJ

Ab Version 3.7.0 hat AssertJ Assertions hinzugefügt isCloseTo, wenn Sie die Java 8 Date / Time API verwenden.

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));

Es funktioniert auch mit alten Java-Daten:

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

Dies ist die Lösung, nach der ich gesucht habe :)
Geoaxis

1
Danke, das hat mir eine Menge Zeit gespart!
Robert Beltran

Warum nicht DateUtils.round verwenden?
domi

1
Runde würde auch funktionieren. Es wird auf- oder abgerundet, während das Abschneiden immer abfällt. Laut den Dokumenten behandelt round auch die Sommerzeit.
Dan Watt

Ich hatte das gleiche Problem mit java.sql.Timestampsund DateUtils.truncate(...)arbeitete für mich in Java 8. Mein spezieller Fall beinhaltete eine Datenbanktechnologie, die das Speichern von feinerem Korn als einer Sekunde nicht unterstützte, also verglich ich einen speicherinternen Zeitstempel mit einem gespeicherten und aus einer Datenbank abgerufen. Der speicherinterne Zeitstempel hatte eine höhere Genauigkeit als der aus der Datenbank gelesene Zeitstempel.
Kent Bull

6

Sie könnten so etwas tun:

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

Keine String-Vergleiche erforderlich.


Ich denke du meintest "/" gegen "%"? Dies wird meiner Meinung nach in Bezug auf willkürliche Präzision chaotisch. Guter Punkt.
Michael Easter

Hoppla! Guter Fang. Ich denke jedoch nicht, dass Präzision ein Problem ist. Date.getTime () gibt seit der Epoche immer eine lange ms zurück.
Seth

1
Dies schlägt fehl, wenn ein Wert 3,999 Sekunden und der andere 4.000 Sekunden beträgt. Mit anderen Worten, manchmal toleriert es einen Unterschied von bis zu einer Sekunde, manchmal schlägt es für einen Unterschied von 2 ms fehl.
David Balažic

6

In JUnit können Sie zwei Assert-Methoden wie folgt programmieren:

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

Ich weiß nicht, ob es Unterstützung in JUnit gibt, aber eine Möglichkeit, dies zu tun:

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!"); }
    }
}

Wenn Sie die Methode aufrufen, assertEqualDateswürde ich ihren Rückgabetyp voidund die letzte Zeile erstellen assertEquals(d1, d2). Auf diese Weise würde es sich wie alle JUnit- assert*Methoden verhalten .
Joachim Sauer

Einverstanden. Ich wollte den Code ausführen und hatte JUnit nicht zur Hand.
Michael Easter

1
Seien Sie vorsichtig bei globalen Datumsformatierern. Sie sind nicht threadsicher. Es ist kein Problem mit diesem Code, aber es ist eine schlechte Angewohnheit zu haben.
Itsadok

1
Dies ist nicht der Fall, wenn die beiden Datumsobjekte einen Unterschied von weniger als einer Sekunde aufweisen, aber den zweiten Schwellenwert überschreiten.
Ophidian

3

Dies ist tatsächlich ein schwierigeres Problem, als es aufgrund der Grenzfälle erscheint, in denen die Varianz, die Sie nicht interessieren, einen Schwellenwert für einen Wert überschreitet, den Sie überprüfen. Beispielsweise beträgt die Millisekunden-Differenz weniger als eine Sekunde, aber die beiden Zeitstempel überschreiten die zweite Schwelle oder die Minutenschwelle oder die Stundenschwelle. Dies macht jeden DateFormat-Ansatz von Natur aus fehleranfällig.

Stattdessen würde ich vorschlagen, die tatsächlichen Millisekunden-Zeitstempel zu vergleichen und ein Varianzdelta anzugeben, das angibt, was Sie als akzeptablen Unterschied zwischen den beiden Datumsobjekten betrachten. Es folgt ein übermäßig ausführliches Beispiel:

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

Mit JUnit 4 können Sie auch einen Matcher zum Testen von Daten gemäß der von Ihnen gewählten Genauigkeit implementieren . In diesem Beispiel verwendet der Matcher einen Ausdruck im Zeichenfolgenformat als Parameter. Der Code ist für dieses Beispiel nicht kürzer. Die Matcher-Klasse kann jedoch wiederverwendet werden. und wenn Sie ihm einen beschreibenden Namen geben, können Sie die Absicht mit dem Test auf elegante Weise dokumentieren.

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

Verwenden Sie AssertJ-Zusicherungen für 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()));

Die Meldung "Test fehlgeschlagen" ist besser lesbar

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 funktioniert auch für java.util.date:assertThat(new Date(2016 - 1900, 0, 1,12,13,14)).isEqualToIgnoringMillis("2016-01-01T12:13:14");
Dan Watt


1

Vergleichen Sie einfach die Datumsteile, die Sie vergleichen möchten:

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));

Ich mag diesen Ansatz viel besser als einen Datumsformatierer. Das einzige Problem ist, dass die spezifischen Getterfelder in Datum veraltet sind. Verwenden Sie besser einen Kalender, um dasselbe zu tun.
Kfox

Ah, ein guter Punkt zu beachten, dass diese Methoden veraltet sind. Ich habe meine Antwort mit dem alternativen Code aktualisiert, um stattdessen Kalenderobjekte zu konvertieren und zu vergleichen.
Oliver Hernandez

1

JUnit verfügt über eine integrierte Zusicherung zum Vergleichen von Doppelwerten und zum Festlegen, wie nahe diese sein müssen. In diesem Fall liegt das Delta innerhalb der Anzahl der Millisekunden, die Sie als gleichwertig betrachten. Diese Lösung hat keine Randbedingungen, misst die absolute Varianz, kann leicht die Genauigkeit festlegen und erfordert kein Schreiben zusätzlicher Bibliotheken oder Codes.

    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);

Hinweis Es muss 100,0 anstelle von 100 sein (oder es ist eine Umwandlung in Doppel erforderlich), um zu erzwingen, dass sie als Doppel verglichen werden.


1

Sie können beim Vergleichen von Daten auswählen, welche Genauigkeitsstufe Sie möchten, z.

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

So etwas könnte funktionieren:

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

0

Anstatt new Datedirekt zu verwenden, können Sie einen kleinen Mitarbeiter erstellen, den Sie in Ihrem Test verspotten können:

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

Erstellen Sie ein DateBuilder-Mitglied und ändern Sie die Aufrufe von new DatenachdateBuilder.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();
    }
}

Die Hauptmethode erzeugt:

Dates are the same: false

Im Test können Sie einen Stub von injizieren DateBuilderund ihn einen beliebigen Wert zurückgeben lassen. Zum Beispiel mit Mockito oder einer anonymen Klasse, die Folgendes überschreibt 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

Konvertieren Sie die Daten mit SimpleDateFromat in einen String, geben Sie im Konstruktor die erforderlichen Datums- / Zeitfelder an und vergleichen Sie die String-Werte:

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


0

Hier ist eine Utility-Funktion, die den Job für mich erledigt hat.

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


-1

Ich habe die Objekte in java.util.Date umgewandelt und verglichen

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

Dies führt dazu, dass die Bestätigung aufgrund der Genauigkeit fehlschlägt.
TechCrunch
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.