Skąd bierzecie akutalną datę i jak to testujecie?

0

Żeby pobrać aktualną datę można wykonać Instant.now(), bardzo szybkie i daje aktualną godzinę, jaka jest na zegarku systemowym.
Ale to rozwiązanie jest nietestowalne, nie da się przewidzieć w teście co .now() zwróci, nie da się wykonać asercji. Natomiast niektórzy próbują mockować tę statyczną metodę powermockiem. Proste, prawie czytelne, śmierdzi fest.

Jaka ma całkiem fajną klasę Clock, można wykonać clock.instant() i będzie aktualny czas. W kodzie produkcyjnym można wstrzyknąć systemowy zegarek, a w teście Clock.fixed(...), wtedy w teście możemy przewidzieć jaki czas zostanie zwrócony. Fajnie, bo można dobre asercje robić. Ale trzeba jakieś cloicki wstrzykiwać, dodatkowe zależności, dodatkowe beany produkcyjne, dodatkowe beany w testach integracyjnych. Można pobrać godzinę, ale nie można sterować, nie da się w teście integracyjnym zasymulować upływu czasu.

W niektórych projektach widzę, że dodany jest interfejs

public interface TimeProvider {

    Instant instant();

}

(identyczny interfejs dostarcza java od jakiegoś czasu, InstantSource). W produkcyjnym kodzie jest tam Clock, a w testowym taki bean

public class FakeTimeProvider implements TimeProvider {

    private Clock clock;

    public TestTimeProvider() {
        this(Instant.parse("2021-04-11T13:00:00.000Z"));
    }

    public TestTimeProvider(Instant initInstant) {
        this.clock = buildClock(initInstant);
    }

    @Override
    public Instant instant() {
        return clock.instant();
    }

    public void plus(TemporalAmount temporalAmount) {
        clock =  buildClock(instant().plus(temporalAmount));
    }

    public void plus(long amountToAdd, TemporalUnit unit) {
        clock =  buildClock(instant().plus(amountToAdd, unit));
    }

    private Clock buildClock(Instant instant) {
        return Clock.fixed(instant, ZoneId.of("UTC"));
    }

}

Co pozwala robić rewelacyjne testy integracyjne

    @Test
    void test_stm() {
        //...

        // when
        timeProvider.plus(5, DAYS);

        // then
        assertThat(...)
    }

Możemy w teście integracyjno behawioralnym przetestować, że coś się stało za kilka dni. Rewelacyjna sprawa. Ale robi się z tego typowa Java, interfejs co przykrywa interfejs i niepotrzbne implementacja, wstrzykiwanie beanów których można nie wsytrzykiwać bo można mocka na Instant.now() zrobić.

Jak ten problem się rozwiązuje u Was?

3

A gdzie najlepsza opcja czyli wstrzykuję generator aktualnej dany przez konstruktor a na testy podmieniam implementację?

1

A czym jest Clock/ własny TimeProvider/InstantSource?

Nie rozumiem pytania. Ale jak patrze na Javadoca to o coś takiego jak InstantSource mi chodziło. Chociaż ja używałem w prostszej wersji. Coś w rodzaju:

trait InstantSource {
  def getNow: Instant
}

class InstantSourceImpl extends InstantSource {
  def getNow: Instant = Instant.now
}

//Wiele różnych rodzaji faków z zalezności od potrzeb
class FakeInstantSource(seq: Seq[Instant]) extends InstantSource {
  var count = 0 //sorry za mutowalność, ale na potrzeby testów sobie pozwalam :P
  def getNow: Instant = {
    val result = seq(count)
    count += 1
    result
  }
}

UPDATE - OK nie znam bo to Java 17, a ja już od ponad roku tylko Scala. Ale skoro InstantSource jest teraz w bibliotece standardowej to utwierdza mnie to w przekonaniu że miałem rację a ci co krzyczeli że odpowiedniki InstantSource to przerost formy nad treścią się mylili :D

1

A gdzie opcja używam Instant.now i tego nie testuję? :D

Btw. Dzięki wam się dowiedziałem o dobrym ficzerze z javy 17, dzięki!

4

Czas to parametr.

Jakiekolwiek użycie przez kod Instant.now() czy też (w starszych systemach) new Date() to dyskwalifikacja tego kodu. (bo jest nietestowalny)

2

TimeProvider. Co ciekawe, jest to jeden z pierwszych naprawde dobrych wzorców których się nauczyłem.

0

Na ogół używam Clock jako parametru konstruktora. Lub instant jako parametru metody. Nie widzę sensu tego TimeProvider`, skoro taką rolę pełni właśnie Clock. Masz kilka gotowych implementacji, a jak coś, to wystarczy nadpisać, ewentualnie zamockować jakąś metodę.

1

Nie widzę sensu tego TimeProvider`, skoro taką rolę pełni właśnie Clock.

Clock jest niemutowalny, więc ciężko z nim pracować w testach integracyjnych.

0

@scibi_92: Ok, defaultowe implementacje Clock są niemutowalne, ale co szkodzi, żeby sobie stworzyć coś takiego:

public class TestClock(): Clock() {
    
    override fun getZone(): ZoneId {
        TODO("Not yet implemented")
    }

    override fun withZone(zone: ZoneId?): Clock {
        TODO("Not yet implemented")
    }

    override fun instant(): Instant {
        TODO("Not yet implemented")
    }
}

i zaimplementować sobie te 3 metody zgodnie z aktualną potrzebą? Albo nawet zrobić coś bardziej uniwersalnego, zamiast wprowadzać trochę sztuczny supplier?

1

Dlatego że często to nie Instant jest używany tylko jakiś LocalDate, LocalDateTime etc. a używając TimeProvidera nie musimy za każdym razem korzystać z LocalDate.of(clock). Może też być tak że będziesz głównie bazował na strefie UTC, więc mając metodę getUtcDateTime jest łatwiej ogarnąc w kodzie że to na tej strefie czasowej bazujesz zamiast na strefie systemowej, albo wręcz możesz stosować zarówo UTC jak i lokalna strefę czasową, w zalezności od potrzeb.

0

@scibi_92: Jeżeli bazuję na UTC, to nic innego niż instant nie jest potrzebne, bo z założenia to jest właśnie UTC. Nie wiedziałem o InstanceSource - faktycznie fajniejsze, bo to interface a nie klasa abstrakcyjna.

1 użytkowników online, w tym zalogowanych: 0, gości: 1