Cześć, tak jak w tytule, ogarnąłem jak cache'ować dane ale nie wiem jak sprawdzić czy poprawnie to wykonuje, ma ktoś może jakiś pomysł jak sprawdzić czy cache się na pewno wykonał?
Microbenchmark. Uderzasz milion razy i sprawdzasz czasy. Jak będą i tak i tak niskie to znaczy, że cache niepotrzebny :)
Cacheowanie gdzie? W przeglądarce? Przed bazą? Co to znaczy poprawne cacheowanie? Chodzi o zapis danych, o konfigurację TTL, ...? :)
Będę tutaj zgadywać: masz jakąś warstwę cache nad jakąś bazą danych i chcesz napisać testy, które weryfikują czy dane faktycznie lecą z cache?
- Wykonujesz request ktróry powinien wrzucić dane do bazy
- Psujesz bazę (składasz ją w teście, albo podmieniasz connection na coś innego), podmieniasz połączenie do serwisu z którego normalnie pobierasz dane etc. Robisz coś, co sprawia że nie da się już dostać danych "normalną drogą".
- Wykonujesz kolejny request, który może zadziałać tylko jeśli dane przyjdą z cache
Nie wiem co za cache masz, ale niektóre cache udostępniają API dające możliwość podpięcia własnego listenera. W takim listenerze możesz obsługiwać zdarzenia typu "entryAdded/Expired/Removed/cacheHit/cachMiss/etc.". Twój listener mógłby zbierać statystyki i pokazywać ile razy miałeś cache hit/miss etc.
Żeby nie być gołosłownym: https://www.ehcache.org/apidocs/2.8.4/net/sf/ehcache/statistics/CacheUsageListener.html
Używam mutowania (w testach) i sprawdzam jak często serwis pod spodem jest wywołany - normalnie jak najgorszy mock :-)
https://github.com/neeffect/nee/blob/master/nee-core/src/test/kotlin/pl/setblack/nee/effects/tx/CacheEffectTest.kt
Widać, że jakbym tu użył Mockito, to mógłbym zaoszczędzić jedna linijke kodu :/. A do tego są copy-paste - review nie było.
Czyli - niestety ten case, gdzie poświęcam ideały w imię testowania. Cache jest generalnie przezroczysty z punktu widzenia wartości, czy działa, czy nie działa - funkcja daje ten sam wynik...
i nie widzę sensownego innego sposobu testowania jak na razie. Choć możliwe, że jakiś alternatywny istnieje -pomysł @Shalom jest inny, ale ma ten sam problem z punktu widzenia czystości, jednakowoż chyba widze jak to w jego wersji poprawić... (przyjdzie Reader i wyrówna).
Przykład jak gdzieś coś podobnego testowałem:
// given
testConfiguration = testHelper.createNewConfiguration()
.withRandomUser()
.withRandomFile()
.grantUserAccessToFiles()
.setup();
//when
Either<RestRequestFailure, ArchiveFiles> result = client.getAccessibleFiles(testConfiguration.getUser().getId(), testConfiguration.getFileIds());
assertTrue(result.isRight());
assertFalse(result.get().getFiles().isEmpty());
testHelper.withDBFailure(() -> {
Either<RestRequestFailure, ArchiveFiles> cachedResult = client.getAccessibleFiles(testConfiguration.getUser().getId(), testConfiguration.getFileIds());
// then
assertTrue(cachedResult.isRight());
assertFalse(cachedResult.get().getFiles().isEmpty());
});
A samo failure symulowane przez takiego potworka:
public void withDBFailure(Runnable testCode) {
JdbcTemplate fakeJdbc = mock(JdbcTemplate.class);
Whitebox.setInternalState(fileAccessRepository, JdbcTemplate.class, fakeJdbc);
when(fakeJdbc.query(anyString(), any(Object[].class), any(RowMapper.class))).thenThrow(new RuntimeException());
testCode.run();
Whitebox.setInternalState(fileAccessRepository, JdbcTemplate.class, bookkepingJdbc);
}
Może sama implementacja nie jest jakoś strasznie "czysta", ale za to sam test jest dość czytelny. Mamy takie closure w którym operacje na bazie failują i możemy sobie w nim sprawdzać zachowanie kiedy baza leży.
Cache w API
strzelasz do endpointa i jeżeli response przyjdzie w < 10ms to przyszedł z cache, a jak nie to poszło do prawdziwej bazki
nie ma lepszego testu :D
i faktycznie testuje aplikacje, a nie 10 metrów kodu do fakeowania
a jak chcesz uniknąć false positive to niech bazka będzie wypełniona danymi, wtedy różnica pomiędzy cache i bez będzie coraz większa
WeiXiao napisał(a):
strzelasz do endpointa i jeżeli response przyjdzie w < 10ms to przyszedł z cache, a jak nie to poszło do prawdziwej bazki
Ale można też cechować przecież niżej niż cały response i może trwać on więcej niż 10ms
Sztuka dla sztuki. Zainspirowany podejściem @Shalom
zmieniłem mój test cache, tak żeby mniej używać mutacji
https://github.com/jarekratajski/nee/blob/develop/nee-core/src/test/kotlin/pl/setblack/nee/effects/cache/CacheEffectTest.kt
internal class CacheEffectTest : BehaviorSpec({
Given("cache effect and naive implementation") {
val cacheProvider = NaiveCacheProvider()
val cache = CacheEffect<Env, Nothing>(cacheProvider)
When("function called twice using same param and different env") {
val businessFunction =
Nee.pure(
cache, ::returnEnvIgnoringParam)
val x1 = businessFunction.perform(env = Env.SomeValue)(1)
val x2 = businessFunction.perform(env = Env.OtherValue)(1)
Then("second call should ignore different env") {
x2.get() shouldBe x1.get()
}
Then("second call should return first stored value") {
x2.get() shouldBe Env.SomeValue
}
}
When("function called twice using different params and env") {
val businessFunction =
Nee.pure(
cache, ::returnEnvIgnoringParam)
val x2 = businessFunction.perform(env = Env.SomeValue)(1).flatMap { _ ->
businessFunction.perform(env = Env.OtherValue)(2)
}
Then("second call should return other env value") {
x2.get() shouldBe Env.OtherValue
}
}
}
})
fun returnEnvIgnoringParam(env:Env) = { _:Int -> env}
sealed class Env {
object SomeValue : Env()
object OtherValue: Env()
}
Nadal sam cache (testowy) polega na concurrenthashMap :-(, więc pięknie nie jest. Dla zadających trudne pytania: To nie jest test Cache, ale test efektu/ aspektu cache. Gdzie Implementacja defaultowa to Caffeine.
Przyjąłem, że Caffeine ktoś już potestował....
Da się zrobić to (efekt i testy) zupełnie czysto, ale wtedy zrobiłbym prawdopodobnie bardzo czysty, funkcyjny i zupełnie bezużyteczny cache - w Kotlinie nie będe się w to bawił.