Porównaj obiekty Date z różnymi poziomami dokładności


81

Mam test JUnit, który kończy się niepowodzeniem, ponieważ milisekundy są różne. W tym przypadku nie obchodzą mnie milisekundy. Jak mogę zmienić precyzję potwierdzenia, aby ignorować milisekundy (lub dowolną precyzję, którą chciałbym ustawić)?

Przykład niezgodnego twierdzenia, które chciałbym spełnić:

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

Odpowiedzi:


22

Użyj DateFormatobiektu z formatem, który pokazuje tylko części, które chcesz dopasować i wykonaj assertEquals()na wynikowych Ciągach. Możesz również łatwo zawinąć to w swoją własną assertDatesAlmostEqual()metodę.


15
Nie obsługuje przypadku różnicy milisekund na drugiej granicy, 10.000 i 09.999 byłyby różne.
scarba05

61

Jeszcze jedno obejście, zrobiłbym to w ten sposób:

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

4
+1 do porównania wariancji, ale nie uwzględnia wariancji bezwzględnej (np. Co, jeśli data1 jest późniejsza niż data2?)
Ophidian

13
Zwykle stosuję podobne podejście, po prostu pakując to za pomocą Math.abs ()
parxier

60

Istnieją biblioteki, które pomagają w tym:

Apache commons-lang

Jeśli masz Apache commons-lang na swojej ścieżce klas, możesz użyć DateUtils.truncatedo skrócenia dat do jakiegoś pola.

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

Jest na to skrót:

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

Pamiętaj, że godziny 12: 00: 00.001 i 11: 59: 00.999 zostałyby obcięte do innych wartości, więc może to nie być idealne rozwiązanie. Do tego jest okrągły:

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

AssertJ

Począwszy od wersji 3.7.0, AssertJ dodał isCloseToasercje, jeśli używasz interfejsu API daty / czasu 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));

Działa również ze starszymi datami Java:

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

to jest rozwiązanie, którego szukałem :)
geoaxis

1
Dzięki temu zaoszczędziłem mnóstwo czasu!
Robert Beltran

Dlaczego nie użyć DateUtils.round?
domi

1
Okrągły by też działał. Zaokrągla się w górę lub w dół, podczas gdy obcięcie zawsze spada. Zgodnie z dokumentacją , round obsługuje również czas letni.
Dan Watt

Miałem ten sam problem z java.sql.Timestampsi DateUtils.truncate(...)pracował dla mnie w Javie 8. Mój szczególny przypadek obejmował technologii baz danych, które nie obsługuje zapisywania jakichkolwiek ziarno drobniejsze niż sekundę, więc miałem porównanie znacznik czasu w pamięci do jednego, który został zapisany pobrane z bazy danych. Znacznik czasu w pamięci miał większą dokładność niż znacznik czasu odczytany z bazy danych.
Kent Bull

6

Możesz zrobić coś takiego:

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

Nie są potrzebne porównania ciągów.


Myślę, że miałeś na myśli „/” kontra „%”? Robi się bałagan z powodu arbitralnej precyzji, IMHO. Słuszna uwaga.
Wielkanoc Michaela

Ups! Dobry chwyt. Nie wydaje mi się jednak, żeby precyzja była problemem. Date.getTime () zawsze zwraca długi ms od epoki.
Seth

1
To się nie powiedzie, jeśli jedna wartość to 3,999 sekundy, a druga 4000. Innymi słowy, czasami będzie tolerować różnicę do kilku sekund, czasami zawiedzie przy różnicy 2 ms.
David Balažic

6

W JUnit możesz zaprogramować dwie metody asercji, takie jak ta:

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

Nie wiem, czy w JUnit jest wsparcie, ale można to zrobić w jeden sposób:

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

Jeśli wywołasz metodę assertEqualDates, zrobię jej typ zwracania voidi utworzę ostatnią linię assertEquals(d1, d2). W ten sposób zachowywałby się tak samo, jak wszystkie assert*metody JUnit .
Joachim Sauer

Zgoda. Chciałem uruchomić kod i nie miałem pod ręką JUnita.
Wielkanoc Michaela

1
Uważaj na globalne elementy formatujące datę. Nie są bezpieczne dla wątków. To nie jest problem z tym kodem, ale to zły nawyk.
itsadok

1
Nie obsługuje to przypadku, w którym dwa obiekty Date mają różnicę poniżej sekundy, ale przekraczają drugi próg.
Ophidian

3

W rzeczywistości jest to trudniejszy problem, niż się wydaje, z powodu przypadków granicznych, w których wariancja, na której nie zależy Ci, przekracza próg dla sprawdzanej wartości. np. różnica milisekund jest mniejsza niż sekunda, ale dwa znaczniki czasu przekraczają drugi próg, próg minut lub próg godzin. To sprawia, że ​​każde podejście DateFormat jest z natury podatne na błędy.

Zamiast tego sugerowałbym porównanie rzeczywistych milisekundowych znaczników czasu i podanie delty wariancji wskazującej, jaka jest akceptowalna różnica między dwoma obiektami daty. Oto nadmiernie rozwlekły przykład:

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

Korzystając z JUnit 4, możesz również zaimplementować dopasowanie do dat testowania zgodnie z wybraną precyzją. W tym przykładzie element dopasowujący przyjmuje jako parametr wyrażenie formatu ciągu. W tym przykładzie kod nie jest krótszy. Jednak klasa dopasowania może zostać ponownie wykorzystana; a jeśli nadasz mu opisową nazwę, możesz w elegancki sposób udokumentować zamiar za pomocą testu.

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

użyj asercji AssertJ dla 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()));

komunikat o niepowodzeniu testu jest bardziej czytelny

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 działa również dla java.util.date:assertThat(new Date(2016 - 1900, 0, 1,12,13,14)).isEqualToIgnoringMillis("2016-01-01T12:13:14");
Dan Watt


1

Po prostu porównaj części dat, które chcesz porównać:

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

Takie podejście podoba mi się o wiele lepiej niż użycie programu formatującego datę. Jedynym problemem jest to, że określone pola pobierające w Date są przestarzałe. Lepiej jest użyć Kalendarza, aby zrobić to samo.
kfox

Ach, warto zauważyć, że te metody są przestarzałe. Zaktualizowałem moją odpowiedź alternatywnym kodem, aby zamiast tego konwertować i porównywać obiekty kalendarza.
Oliver Hernandez

1

JUnit ma wbudowaną asercję do porównywania podwójnych i określania, jak blisko muszą się znajdować. W tym przypadku delta mieści się w zakresie milisekund, które uznajesz za równoważne. To rozwiązanie nie ma warunków brzegowych, mierzy bezwzględną wariancję, może z łatwością określić precyzję i nie wymaga pisania dodatkowych bibliotek ani kodu.

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

Uwaga Musi być 100,0 zamiast 100 (lub wymagane jest rzutowanie na podwojenie), aby zmusić go do porównania ich jako podwójnych.


1

Podczas porównywania dat możesz wybrać żądany poziom dokładności, np .:

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

Coś takiego może działać:

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

0

Zamiast używać new Datebezpośrednio, możesz utworzyć małego współpracownika, którego możesz wyszydzić w swoim teście:

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

Utwórz składową DateBuilder i zmień wywołania z new DatenadateBuilder.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();
    }
}

Główna metoda da:

Dates are the same: false

W teście możesz wstrzyknąć kod DateBuilderi pozwolić mu zwrócić dowolną wartość. Na przykład z Mockito lub anonimową klasą, która zastępuje 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

Konwertuj daty na ciąg przy użyciu SimpleDateFromat, określ w konstruktorze wymagane pola daty / godziny i porównaj wartości ciągów:

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


0

Oto funkcja narzędziowa, która wykonała pracę za mnie.

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


-1

rzucam obiekty na java.util.Date i porównuję

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

Spowoduje to niepowodzenie potwierdzenia z powodu precyzji.
TechCrunch,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.