-
-
Notifications
You must be signed in to change notification settings - Fork 108
Add initial unit test coverage for Metrics event persistence #1439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,4 +53,7 @@ private void processEvent(String event, Instant happenedAt) { | |
| .insert()); | ||
| } | ||
|
|
||
| public ExecutorService getExecutorService() { | ||
| return service; | ||
| } | ||
|
Comment on lines
+56
to
+58
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undo, this won't be needed (see other comment why) |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| package org.togetherjava.tjbot.features; | ||
|
|
||
| import org.junit.jupiter.api.AfterEach; | ||
| import org.junit.jupiter.api.BeforeEach; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import org.togetherjava.tjbot.db.Database; | ||
| import org.togetherjava.tjbot.db.generated.tables.MetricEvents; | ||
| import org.togetherjava.tjbot.features.analytics.Metrics; | ||
|
|
||
| import java.time.Duration; | ||
| import java.time.Instant; | ||
| import java.time.temporal.ChronoUnit; | ||
| import java.util.List; | ||
| import java.util.concurrent.locks.LockSupport; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.within; | ||
| import static org.junit.jupiter.api.Assertions.fail; | ||
|
|
||
| final class MetricsTests { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this test is too much in terms of complexity for no tangible benefit. lets step back and create something simpler that beginners can understand and maintain while providing pretty much the same test-safety in terms of relevant coverage.
@Test
void basicEventCount() {
// GIVEN a test event
String expectedEvent = "test";
Instant expectedHappenedAt = Instant.now();
// WHEN counting the event
metrics.countBlocking(expectedEvent);
// THEN the event was saved in the DB
Foo entry = database...
String actualEvent = entry.event();
Instant actualHappenedAt = entry.happenedAt();
assertEquals(expectedEvent, actualEvent);
assertCloseEnough(expectedHappenedAt, actualHappenedAt);
}
private static void assertCloseEnough(Instant expected, Instant actual) {
... // assert that its within 1min difference
} (double check the correct order of expected vs actual in assert, i never can remember it, lol) |
||
| private static final Logger logger = LoggerFactory.getLogger(MetricsTests.class); | ||
|
|
||
| private static final Duration WAIT_TIMEOUT = Duration.ofSeconds(3); | ||
|
|
||
| private Database database; | ||
| private Metrics metrics; | ||
|
|
||
| @BeforeEach | ||
| void setUp() { | ||
| database = Database.createMemoryDatabase(MetricEvents.METRIC_EVENTS); | ||
| metrics = new Metrics(database); | ||
| } | ||
|
|
||
| @AfterEach | ||
| void tearDown() { | ||
| metrics.getExecutorService().shutdownNow(); | ||
| } | ||
|
|
||
| @Test | ||
| void countInsertsSingleEvent() { | ||
|
|
||
| final String slashPing = "slash-ping"; | ||
|
|
||
| metrics.count(slashPing); | ||
|
|
||
| awaitRecords(1); | ||
|
|
||
| List<String> recordedEvents = readEventsOrderedById(); | ||
|
|
||
| assertThat(recordedEvents).as("Metrics should persist the counted event in insertion order") | ||
| .containsExactly(slashPing); | ||
|
|
||
| assertThat(readLatestEventHappenedAt()) | ||
| .as("Metrics should store a recent timestamp for event '%s' (recordedEvents=%s)", | ||
| slashPing, recordedEvents) | ||
| .isNotNull() | ||
| .isCloseTo(Instant.now(), within(5, ChronoUnit.SECONDS)); | ||
| } | ||
|
|
||
| private void awaitRecords(int expectedAmount) { | ||
| Instant deadline = Instant.now().plus(WAIT_TIMEOUT); | ||
|
|
||
| while (Instant.now().isBefore(deadline)) { | ||
| if (readRecordCount() == expectedAmount) { | ||
| return; | ||
| } | ||
|
|
||
| LockSupport.parkNanos(Duration.ofMillis(25).toNanos()); | ||
|
|
||
| if (Thread.interrupted()) { | ||
| int actualCount = readRecordCount(); | ||
|
|
||
| String msg = String.format( | ||
| "Interrupted while waiting for metrics writes (expectedAmount=%d, actualCount=%d, timeout=%s, events=%s)", | ||
| expectedAmount, actualCount, WAIT_TIMEOUT, readEventsOrderedById()); | ||
|
|
||
| logger.warn(msg); | ||
|
|
||
| fail(msg); | ||
| } | ||
| } | ||
|
|
||
| int actualCount = readRecordCount(); | ||
|
|
||
| List<String> recordedEvents = readEventsOrderedById(); | ||
|
|
||
| String timeoutMessage = String.format( | ||
| "Timed out waiting for metrics writes (expectedAmount=%d, actualCount=%d, timeout=%s, events=%s)", | ||
| expectedAmount, actualCount, WAIT_TIMEOUT, recordedEvents); | ||
|
|
||
| logger.warn(timeoutMessage); | ||
|
|
||
| assertThat(actualCount).as(timeoutMessage).isEqualTo(expectedAmount); | ||
| } | ||
|
|
||
| private int readRecordCount() { | ||
| return database.read(context -> context.fetchCount(MetricEvents.METRIC_EVENTS)); | ||
| } | ||
|
|
||
| private List<String> readEventsOrderedById() { | ||
| return database.read(context -> context.select(MetricEvents.METRIC_EVENTS.EVENT) | ||
| .from(MetricEvents.METRIC_EVENTS) | ||
| .orderBy(MetricEvents.METRIC_EVENTS.ID.asc()) | ||
| .fetch(MetricEvents.METRIC_EVENTS.EVENT)); | ||
|
|
||
| } | ||
|
|
||
| private Instant readLatestEventHappenedAt() { | ||
| return database.read(context -> context.select(MetricEvents.METRIC_EVENTS.HAPPENED_AT) | ||
| .from(MetricEvents.METRIC_EVENTS) | ||
| .orderBy(MetricEvents.METRIC_EVENTS.ID.desc()) | ||
| .limit(1) | ||
| .fetchOne(MetricEvents.METRIC_EVENTS.HAPPENED_AT)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,8 @@ | |
| </RollingFile> | ||
| </Appenders> | ||
| <Loggers> | ||
| <Logger name="org.togetherjava.tjbot" level="debug"/> | ||
|
|
||
|
Comment on lines
+16
to
+17
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. undo |
||
| <Root level="info"> | ||
| <AppenderRef ref="Console"/> | ||
| <AppenderRef ref="File"/> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
undo. we want to use raw junit in this project for now as it is easier for beginners and reflects more what they will see in the wild and might already be familiar with from their own projects and university etc