Files
java-design-patterns/health-check/src/test/java/AsynchronousHealthCheckerTest.java
T
Doksanbir 21f7b026f5 pattern: Implement Health Check for Microservices Observability (#2695) (#2774)
* Add Health Check pattern implementation

The commit introduces  Health Check pattern, providing a series of health indicators for system performance and stability monitoring, including checks for system CPU load, process CPU load, database health, memory usage, and garbage collection metrics. It also includes asynchronous execution and caching mechanisms for health checks, and retry configurations for resilience.

Implements health checking components as per issue #2695.

* Test cases and javadoc for HealthEndpointIntegrationTest

* Added more log to test case to see why it returns 503

* Change config values to see if the system High system CPU load is resolved or not in CI.

* Fixes for test cases.

* some fixes for Sonar.

* some fixes for Sonar.
ADDED HIGH_PROCESS_CPU_LOAD_MESSAGE_WITHOUT_PARAM
ADDED HIGH_SYSTEM_CPU_LOAD_MESSAGE_WITHOUT_PARAM

* Sonar fixes address "Define and throw a dedicated exception instead of using a generic one."

added HealthCheckInterruptedException
refactored CustomHealthIndicator

* fixes checkstyle violation.
2023-12-02 14:17:01 +02:00

224 lines
7.8 KiB
Java

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.iluwatar.health.check.AsynchronousHealthChecker;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.*;
import java.util.function.Supplier;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
/**
* Tests for {@link AsynchronousHealthChecker}.
*
* @author ydoksanbir
*/
@Slf4j
class AsynchronousHealthCheckerTest {
/** The {@link AsynchronousHealthChecker} instance to be tested. */
private AsynchronousHealthChecker healthChecker;
private ListAppender<ILoggingEvent> listAppender;
@Mock private ScheduledExecutorService executorService;
public AsynchronousHealthCheckerTest() {
MockitoAnnotations.openMocks(this);
}
/**
* Sets up the test environment before each test method.
*
* <p>Creates a new {@link AsynchronousHealthChecker} instance.
*/
@BeforeEach
void setUp() {
healthChecker = new AsynchronousHealthChecker();
// Replace the logger with the root logger of logback
LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
// Create and start a ListAppender
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
listAppender = new ListAppender<>();
listAppender.start();
// Add the appender to the root logger context
loggerContext.getLogger(Logger.ROOT_LOGGER_NAME).addAppender(listAppender);
}
/**
* Tears down the test environment after each test method.
*
* <p>Shuts down the {@link AsynchronousHealthChecker} instance to prevent resource leaks.
*/
@AfterEach
void tearDown() {
healthChecker.shutdown();
((LoggerContext) LoggerFactory.getILoggerFactory()).reset();
}
/**
* Tests that the {@link performCheck()} method completes normally when the health supplier
* returns a successful health check.
*
* <p>Given a health supplier that returns a healthy status, the test verifies that the {@link
* performCheck()} method completes normally and returns the expected health object.
*/
@Test
void whenPerformCheck_thenCompletesNormally() throws ExecutionException, InterruptedException {
// Given
Supplier<Health> healthSupplier = () -> Health.up().build();
// When
CompletableFuture<Health> healthFuture = healthChecker.performCheck(healthSupplier, 3);
// Then
Health health = healthFuture.get();
assertEquals(Health.up().build(), health);
}
/**
* Tests that the {@link performCheck()} method returns a healthy health status when the health
* supplier returns a healthy status.
*
* <p>Given a health supplier that returns a healthy status, the test verifies that the {@link
* performCheck()} method returns a health object with a status of UP.
*/
@Test
void whenHealthCheckIsSuccessful_ReturnsHealthy()
throws ExecutionException, InterruptedException {
// Arrange
Supplier<Health> healthSupplier = () -> Health.up().build();
// Act
CompletableFuture<Health> healthFuture = healthChecker.performCheck(healthSupplier, 4);
// Assert
assertEquals(Status.UP, healthFuture.get().getStatus());
}
/**
* Tests that the {@link performCheck()} method rejects new tasks after the {@link shutdown()}
* method is called.
*
* <p>Given the {@link AsynchronousHealthChecker} instance is shut down, the test verifies that
* the {@link performCheck()} method throws a {@link RejectedExecutionException} when attempting
* to submit a new health check task.
*/
@Test
void whenShutdown_thenRejectsNewTasks() {
// Given
healthChecker.shutdown();
// When/Then
assertThrows(
RejectedExecutionException.class,
() -> healthChecker.performCheck(() -> Health.up().build(), 2),
"Expected to throw RejectedExecutionException but did not");
}
/**
* Tests that the {@link performCheck()} method returns a healthy health status when the health
* supplier returns a healthy status.
*
* <p>Given a health supplier that throws a RuntimeException, the test verifies that the {@link
* performCheck()} method returns a health object with a status of DOWN and an error message
* containing the exception message.
*/
@Test
void whenHealthCheckThrowsException_thenReturnsDown() {
// Arrange
Supplier<Health> healthSupplier =
() -> {
throw new RuntimeException("Health check failed");
};
// Act
CompletableFuture<Health> healthFuture = healthChecker.performCheck(healthSupplier, 10);
// Assert
Health health = healthFuture.join();
assertEquals(Status.DOWN, health.getStatus());
String errorMessage = health.getDetails().get("error").toString();
assertTrue(errorMessage.contains("Health check failed"));
}
/**
* Helper method to check if the log contains a specific message.
*
* @param action The action that triggers the log statement.
* @return True if the log contains the message after the action is performed, false otherwise.
*/
private boolean doesLogContainMessage(Runnable action) {
action.run();
List<ILoggingEvent> events = listAppender.list;
return events.stream()
.anyMatch(event -> event.getMessage().contains("Health check executor did not terminate"));
}
/**
* Tests that the {@link AsynchronousHealthChecker#shutdown()} method logs an error message when
* the executor does not terminate after attempting to cancel tasks.
*/
@Test
void whenShutdownExecutorDoesNotTerminateAfterCanceling_LogsErrorMessage() {
// Given
healthChecker.shutdown(); // To trigger the scenario
// When/Then
boolean containsMessage = doesLogContainMessage(healthChecker::shutdown);
if (!containsMessage) {
List<ch.qos.logback.classic.spi.ILoggingEvent> events = listAppender.list;
LOGGER.info("Logged events:");
for (ch.qos.logback.classic.spi.ILoggingEvent event : events) {
LOGGER.info(event.getMessage());
}
}
assertTrue(containsMessage, "Expected log message not found");
}
/**
* Verifies that {@link AsynchronousHealthChecker#awaitTerminationWithTimeout} returns true even
* if the executor service does not terminate completely within the specified timeout.
*
* @throws NoSuchMethodException if the private method cannot be accessed.
* @throws InvocationTargetException if the private method throws an exception.
* @throws IllegalAccessException if the private method is not accessible.
* @throws InterruptedException if the thread is interrupted while waiting for the executor
* service to terminate.
*/
@Test
void awaitTerminationWithTimeout_IncompleteTermination_ReturnsTrue()
throws NoSuchMethodException,
InvocationTargetException,
IllegalAccessException,
InterruptedException {
// Mock executor service to return false (incomplete termination)
when(executorService.awaitTermination(5, TimeUnit.SECONDS)).thenReturn(false);
// Use reflection to access the private method for code coverage.
Method privateMethod =
AsynchronousHealthChecker.class.getDeclaredMethod("awaitTerminationWithTimeout");
privateMethod.setAccessible(true);
// When
boolean result = (boolean) privateMethod.invoke(healthChecker);
// Then
assertTrue(result, "Termination should be incomplete");
}
}