Skip to content

Commit

Permalink
Add unit tests with mockito for metric stats (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
sschnabe authored May 2, 2023
1 parent cfce0de commit 99a01e5
Show file tree
Hide file tree
Showing 6 changed files with 424 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ public void init(Scope config) {}
@Override
public void postInit(KeycloakSessionFactory factory) {

if (!"true".equals(System.getenv().get("KC_METRICS_STATS_ENABLED"))) {
if (!"true".equals(getenv("KC_METRICS_STATS_ENABLED"))) {
log.infov("Keycloak stats not enabled.");
return;
}

var intervalDuration = Optional
.ofNullable(System.getenv("KC_METRICS_STATS_INTERVAL"))
.ofNullable(getenv("KC_METRICS_STATS_INTERVAL"))
.map(Duration::parse)
.orElse(Duration.ofSeconds(60));
var infoThreshold = Optional
.ofNullable(System.getenv("KC_METRICS_STATS_INFO_THRESHOLD"))
.ofNullable(getenv("KC_METRICS_STATS_INFO_THRESHOLD"))
.map(Duration::parse)
.orElse(Duration.ofMillis(Double.valueOf(intervalDuration.toMillis() * 0.5).longValue()));
var warnThreshold = Optional
.ofNullable(System.getenv("KC_METRICS_STATS_WARN_THRESHOLD"))
.ofNullable(getenv("KC_METRICS_STATS_WARN_THRESHOLD"))
.map(Duration::parse)
.orElse(Duration.ofMillis(Double.valueOf(intervalDuration.toMillis() * 0.75).longValue()));
log.infov("Keycloak stats enabled with interval of {0} and info/warn after {1}/{2}.",
Expand All @@ -64,4 +64,8 @@ public MetricsStatsTask create(KeycloakSession session) {

@Override
public void close() {}

String getenv(String key) {
return System.getenv().get(key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void run(KeycloakSession session) {
scrape(session);
} catch (Exception e) {
if (e instanceof org.hibernate.exception.SQLGrammarException) {
log.infov("Metrics status task skipped, database not ready");
log.infov("Metrics status task skipped, database not ready.");
} else {
log.errorv(e, "Failed to scrape stats.");
}
Expand All @@ -52,13 +52,13 @@ public void run(KeycloakSession session) {

var duration = Duration.between(start, Instant.now());
if (duration.compareTo(interval) > 0) {
log.errorv("Finished scrapping keycloak stats in {0}, consider to increase interval", duration);
log.errorv("Finished scrapping keycloak stats in {0}, consider to increase interval.", duration);
} else if (duration.compareTo(warnThreshold) > 0) {
log.warnv("Finished scrapping keycloak stats in {0}, consider to increase interval", duration);
log.warnv("Finished scrapping keycloak stats in {0}, consider to increase interval.", duration);
} else if (duration.compareTo(infoThreshold) > 0) {
log.infov("Finished scrapping keycloak stats in {0}", duration);
log.infov("Finished scrapping keycloak stats in {0}.", duration);
} else {
log.debugv("Finished scrapping keycloak stats in {0}", duration);
log.debugv("Finished scrapping keycloak stats in {0}.", duration);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package io.kokuwa.keycloak.metrics.junit;

import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.MethodOrderer;
Expand All @@ -21,9 +30,42 @@
@TestMethodOrder(MethodOrderer.DisplayName.class)
public abstract class AbstractMockitoTest {

private static final List<LogRecord> LOGS = new ArrayList<>();

static {

System.setProperty("org.jboss.logging.provider", "jdk");
System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tT %4$-5s %2$s %5$s%6$s%n");

Logger.getLogger("org.junit").setLevel(Level.INFO);
Logger.getLogger("").setLevel(Level.ALL);
Logger.getLogger("").addHandler(new Handler() {

@Override
public void publish(LogRecord log) {
LOGS.add(log);
}

@Override
public void flush() {}

@Override
public void close() {}
});
}

@BeforeEach
void reset() {
Metrics.globalRegistry.clear();
Metrics.addRegistry(new SimpleMeterRegistry());
LOGS.clear();
}

public static void assertLog(Level level, String message) {
assertTrue(LOGS.stream()
.filter(l -> l.getLevel().equals(level))
.filter(l -> l.getMessage().equals(message))
.findAny().isPresent(),
"log with level " + level + " and message " + message + " not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.kokuwa.keycloak.metrics.stats;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.time.Duration;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.util.ReflectionUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.KeycloakTransactionManager;
import org.keycloak.timer.TimerProvider;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.Spy;

import io.kokuwa.keycloak.metrics.junit.AbstractMockitoTest;

/**
* Test for {@link MetricsStatsFactory} with Mockito.
*
* @author Stephan Schnabel
*/
@DisplayName("metrics: factory")
public class MetricsStatsFactoryTest extends AbstractMockitoTest {

@Spy
MetricsStatsFactoryImpl factory;
@Mock
KeycloakSessionFactory sessionFactory;
@Mock
KeycloakSession session;

@DisplayName("disabled")
@Test
void disabled() {
factory.init(null);
factory.postInit(sessionFactory);
assertNull(factory.create(session));
factory.close();
}

@DisplayName("enabled - with default values")
@Test
void enabledDefault() {
when(factory.getenv("KC_METRICS_STATS_ENABLED")).thenReturn("true");
when(factory.getenv("KC_METRICS_STATS_INTERVAL")).thenReturn(null);
when(factory.getenv("KC_METRICS_STATS_INFO_THRESHOLD")).thenReturn(null);
when(factory.getenv("KC_METRICS_STATS_WARN_THRESHOLD")).thenReturn(null);
assertTask(Duration.ofSeconds(60), Duration.ofSeconds(30), Duration.ofSeconds(45));
}

@DisplayName("enabled - with custom interval")
@Test
void enabledCustomInterval() {
when(factory.getenv("KC_METRICS_STATS_ENABLED")).thenReturn("true");
when(factory.getenv("KC_METRICS_STATS_INTERVAL")).thenReturn("PT300s");
when(factory.getenv("KC_METRICS_STATS_INFO_THRESHOLD")).thenReturn(null);
when(factory.getenv("KC_METRICS_STATS_WARN_THRESHOLD")).thenReturn(null);
assertTask(Duration.ofSeconds(300), Duration.ofSeconds(150), Duration.ofSeconds(225));
}

@DisplayName("enabled - with custom thresholds")
@Test
void enabledCustomThresholds() {
when(factory.getenv("KC_METRICS_STATS_ENABLED")).thenReturn("true");
when(factory.getenv("KC_METRICS_STATS_INTERVAL")).thenReturn(null);
when(factory.getenv("KC_METRICS_STATS_INFO_THRESHOLD")).thenReturn("PT40s");
when(factory.getenv("KC_METRICS_STATS_WARN_THRESHOLD")).thenReturn("PT50s");
assertTask(Duration.ofSeconds(60), Duration.ofSeconds(40), Duration.ofSeconds(50));
}

private void assertTask(Duration interval, Duration infoThreshold, Duration warnThreshold) {

var timerProvider = mock(TimerProvider.class);
when(sessionFactory.create()).thenReturn(session);
when(session.getProvider(TimerProvider.class)).thenReturn(timerProvider);
when(session.getTransactionManager()).thenReturn(mock(KeycloakTransactionManager.class));

factory.postInit(sessionFactory);

var taskCaptor = ArgumentCaptor.forClass(MetricsStatsTask.class);
verify(timerProvider).scheduleTask(
taskCaptor.capture(),
ArgumentMatchers.eq(interval.toMillis()),
ArgumentMatchers.eq("metrics"));
assertNotNull(taskCaptor.getValue(), "task");
assertField(interval, taskCaptor.getValue(), "interval");
assertField(infoThreshold, taskCaptor.getValue(), "infoThreshold");
assertField(warnThreshold, taskCaptor.getValue(), "warnThreshold");
}

private void assertField(Duration expected, MetricsStatsTask task, String name) {
assertEquals(
expected,
assertDoesNotThrow(() -> ReflectionUtils.tryToReadFieldValue(MetricsStatsTask.class, name, task).get()),
"field " + name + " invalid");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.kokuwa.keycloak.metrics.stats;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ServiceLoader;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import io.kokuwa.keycloak.metrics.junit.AbstractMockitoTest;

/**
* Test for {@link MetricsStatsSpi} with Mockito.
*
* @author Stephan Schnabel
*/
@DisplayName("metrics: spi")
public class MetricsStatsSpiTest extends AbstractMockitoTest {

@Test
void test() {

var spi = new MetricsStatsSpi();
assertEquals("metrics", spi.getName(), "getName()");
assertFalse(spi.isInternal(), "isInternal()");
assertNotNull(spi.getProviderClass(), "getProviderClass()");
assertTrue(spi.getProviderFactoryClass().isInterface(), "getProviderFactoryClass() - should be an interface");

var factory = ServiceLoader.load(spi.getProviderFactoryClass()).findFirst().orElse(null);
assertNotNull(factory, "failed to read factory with service loader");
assertEquals(MetricsStatsFactoryImpl.class, factory.getClass(), "factory.class");
assertEquals("default", factory.getId(), "factory.id");
}
}
Loading

0 comments on commit 99a01e5

Please sign in to comment.