Availability Testing in Java

Overview

Availability testing ensures that systems remain operational and accessible under various conditions. This includes testing uptime, failover mechanisms, recovery procedures, and performance under load.

Core Testing Framework

1. Availability Test Base Classes

import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
public abstract class AvailabilityTest {
protected final String testName;
protected final Duration timeout;
protected final ScheduledExecutorService scheduler;
public AvailabilityTest(String testName, Duration timeout) {
this.testName = testName;
this.timeout = timeout;
this.scheduler = Executors.newScheduledThreadPool(4);
}
public abstract TestResult execute();
public String getTestName() {
return testName;
}
public Duration getTimeout() {
return timeout;
}
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
public static class TestResult {
private final String testName;
private final boolean success;
private final String message;
private final Duration duration;
private final Instant timestamp;
private final Throwable error;
private final Map<String, Object> metrics;
public TestResult(String testName, boolean success, String message, 
Duration duration, Throwable error) {
this.testName = testName;
this.success = success;
this.message = message;
this.duration = duration;
this.timestamp = Instant.now();
this.error = error;
this.metrics = new ConcurrentHashMap<>();
}
public static TestResult success(String testName, String message, Duration duration) {
return new TestResult(testName, true, message, duration, null);
}
public static TestResult failure(String testName, String message, Duration duration) {
return new TestResult(testName, false, message, duration, null);
}
public static TestResult error(String testName, String message, Duration duration, Throwable error) {
return new TestResult(testName, false, message, duration, error);
}
// Getters
public String getTestName() { return testName; }
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public Duration getDuration() { return duration; }
public Instant getTimestamp() { return timestamp; }
public Throwable getError() { return error; }
public Map<String, Object> getMetrics() { return metrics; }
public void addMetric(String name, Object value) {
metrics.put(name, value);
}
@Override
public String toString() {
return String.format("TestResult{testName='%s', success=%s, duration=%s, message='%s'}",
testName, success, duration, message);
}
}
}

2. HTTP Endpoint Availability Test

import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
import java.util.Map;
public class HttpEndpointTest extends AvailabilityTest {
private final String endpointUrl;
private final RestTemplate restTemplate;
private final int expectedStatus;
private final Map<String, String> expectedHeaders;
private final String expectedContent;
public HttpEndpointTest(String testName, String endpointUrl, Duration timeout) {
this(testName, endpointUrl, timeout, 200, Map.of(), null);
}
public HttpEndpointTest(String testName, String endpointUrl, Duration timeout,
int expectedStatus, Map<String, String> expectedHeaders,
String expectedContent) {
super(testName, timeout);
this.endpointUrl = endpointUrl;
this.restTemplate = new RestTemplate();
this.expectedStatus = expectedStatus;
this.expectedHeaders = expectedHeaders;
this.expectedContent = expectedContent;
}
@Override
public TestResult execute() {
Instant start = Instant.now();
try {
CompletableFuture<ResponseEntity<String>> future = 
CompletableFuture.supplyAsync(() -> 
restTemplate.getForEntity(endpointUrl, String.class), scheduler);
ResponseEntity<String> response = future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
Duration duration = Duration.between(start, Instant.now());
return validateResponse(response, duration);
} catch (TimeoutException e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.failure(testName, 
"Request timed out after " + duration, duration);
} catch (Exception e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.error(testName, 
"Request failed: " + e.getMessage(), duration, e);
}
}
private TestResult validateResponse(ResponseEntity<String> response, Duration duration) {
// Check status code
if (response.getStatusCodeValue() != expectedStatus) {
return TestResult.failure(testName,
String.format("Expected status %d but got %d", 
expectedStatus, response.getStatusCodeValue()), duration);
}
// Check headers
for (Map.Entry<String, String> expectedHeader : expectedHeaders.entrySet()) {
String actualValue = response.getHeaders().getFirst(expectedHeader.getKey());
if (!expectedHeader.getValue().equals(actualValue)) {
return TestResult.failure(testName,
String.format("Header %s mismatch: expected '%s', got '%s'",
expectedHeader.getKey(), expectedHeader.getValue(), actualValue), duration);
}
}
// Check content
if (expectedContent != null && response.getBody() != null) {
if (!response.getBody().contains(expectedContent)) {
return TestResult.failure(testName,
"Response body does not contain expected content", duration);
}
}
TestResult result = TestResult.success(testName, 
String.format("Endpoint responded successfully in %d ms", duration.toMillis()), duration);
// Add metrics
result.addMetric("response_time_ms", duration.toMillis());
result.addMetric("status_code", response.getStatusCodeValue());
result.addMetric("response_size", response.getBody() != null ? response.getBody().length() : 0);
return result;
}
}

3. Database Connectivity Test

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class DatabaseConnectivityTest extends AvailabilityTest {
private final DataSource dataSource;
private final String validationQuery;
private final String expectedResult;
public DatabaseConnectivityTest(String testName, DataSource dataSource, Duration timeout) {
this(testName, dataSource, "SELECT 1", "1", timeout);
}
public DatabaseConnectivityTest(String testName, DataSource dataSource, 
String validationQuery, String expectedResult, Duration timeout) {
super(testName, timeout);
this.dataSource = dataSource;
this.validationQuery = validationQuery;
this.expectedResult = expectedResult;
}
@Override
public TestResult execute() {
Instant start = Instant.now();
try (Connection connection = dataSource.getConnection()) {
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
try (PreparedStatement stmt = connection.prepareStatement(validationQuery);
ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
String result = rs.getString(1);
return expectedResult.equals(result);
}
return false;
} catch (Exception e) {
throw new RuntimeException("Database validation failed", e);
}
}, scheduler);
boolean isValid = future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
Duration duration = Duration.between(start, Instant.now());
if (isValid) {
TestResult result = TestResult.success(testName, 
"Database connectivity verified", duration);
result.addMetric("query_time_ms", duration.toMillis());
return result;
} else {
return TestResult.failure(testName, 
"Database validation query returned unexpected result", duration);
}
} catch (TimeoutException e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.failure(testName, 
"Database query timed out after " + duration, duration);
} catch (Exception e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.error(testName, 
"Database connectivity test failed: " + e.getMessage(), duration, e);
}
}
}

4. Message Queue Availability Test

import javax.jms.*;
import java.util.concurrent.atomic.AtomicReference;
public class MessageQueueTest extends AvailabilityTest {
private final ConnectionFactory connectionFactory;
private final String queueName;
private final String testMessage;
public MessageQueueTest(String testName, ConnectionFactory connectionFactory, 
String queueName, Duration timeout) {
this(testName, connectionFactory, queueName, "TEST_MESSAGE", timeout);
}
public MessageQueueTest(String testName, ConnectionFactory connectionFactory,
String queueName, String testMessage, Duration timeout) {
super(testName, timeout);
this.connectionFactory = connectionFactory;
this.queueName = queueName;
this.testMessage = testMessage;
}
@Override
public TestResult execute() {
Instant start = Instant.now();
AtomicReference<String> receivedMessage = new AtomicReference<>();
try {
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
try (Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) {
Queue queue = session.createQueue(queueName);
// Send test message
MessageProducer producer = session.createProducer(queue);
TextMessage sentMessage = session.createTextMessage(testMessage);
producer.send(sentMessage);
// Receive test message
MessageConsumer consumer = session.createConsumer(queue);
connection.start();
Message message = consumer.receive(5000); // 5 second receive timeout
if (message instanceof TextMessage) {
receivedMessage.set(((TextMessage) message).getText());
return testMessage.equals(receivedMessage.get());
}
return false;
} catch (Exception e) {
throw new RuntimeException("Message queue test failed", e);
}
}, scheduler);
boolean success = future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
Duration duration = Duration.between(start, Instant.now());
if (success) {
TestResult result = TestResult.success(testName, 
"Message queue test completed successfully", duration);
result.addMetric("round_trip_time_ms", duration.toMillis());
return result;
} else {
return TestResult.failure(testName, 
"Message queue test failed - message not properly echoed", duration);
}
} catch (TimeoutException e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.failure(testName, 
"Message queue test timed out after " + duration, duration);
} catch (Exception e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.error(testName, 
"Message queue test failed: " + e.getMessage(), duration, e);
}
}
}

5. Composite Availability Test

import java.util.*;
import java.util.stream.Collectors;
public class CompositeAvailabilityTest extends AvailabilityTest {
private final List<AvailabilityTest> tests;
private final FailureStrategy failureStrategy;
public CompositeAvailabilityTest(String testName, List<AvailabilityTest> tests) {
this(testName, tests, FailureStrategy.ALL_MUST_PASS, Duration.ofSeconds(30));
}
public CompositeAvailabilityTest(String testName, List<AvailabilityTest> tests,
FailureStrategy failureStrategy, Duration timeout) {
super(testName, timeout);
this.tests = new ArrayList<>(tests);
this.failureStrategy = failureStrategy;
}
@Override
public TestResult execute() {
Instant start = Instant.now();
try {
List<CompletableFuture<TestResult>> futures = tests.stream()
.map(test -> CompletableFuture.supplyAsync(test::execute, scheduler))
.collect(Collectors.toList());
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
allFutures.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
List<TestResult> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
Duration duration = Duration.between(start, Instant.now());
return aggregateResults(results, duration);
} catch (TimeoutException e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.failure(testName, 
"Composite test timed out after " + duration, duration);
} catch (Exception e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.error(testName, 
"Composite test failed: " + e.getMessage(), duration, e);
}
}
private TestResult aggregateResults(List<TestResult> results, Duration duration) {
long successCount = results.stream().filter(TestResult::isSuccess).count();
long totalCount = results.size();
boolean overallSuccess = evaluateOverallSuccess(results);
String message = String.format("%d/%d tests passed", successCount, totalCount);
TestResult aggregatedResult = overallSuccess ? 
TestResult.success(testName, message, duration) :
TestResult.failure(testName, message, duration);
// Add detailed results
aggregatedResult.addMetric("total_tests", totalCount);
aggregatedResult.addMetric("passed_tests", successCount);
aggregatedResult.addMetric("failed_tests", totalCount - successCount);
aggregatedResult.addMetric("success_rate", (double) successCount / totalCount * 100);
// Add individual test results
aggregatedResult.addMetric("individual_results", results);
return aggregatedResult;
}
private boolean evaluateOverallSuccess(List<TestResult> results) {
switch (failureStrategy) {
case ALL_MUST_PASS:
return results.stream().allMatch(TestResult::isSuccess);
case AT_LEAST_ONE_MUST_PASS:
return results.stream().anyMatch(TestResult::isSuccess);
case MAJORITY_MUST_PASS:
long successCount = results.stream().filter(TestResult::isSuccess).count();
return successCount > results.size() / 2;
case CUSTOM_THRESHOLD:
long threshold = (long) (results.size() * 0.8); // 80% must pass
long actualSuccess = results.stream().filter(TestResult::isSuccess).count();
return actualSuccess >= threshold;
default:
return results.stream().allMatch(TestResult::isSuccess);
}
}
public void addTest(AvailabilityTest test) {
tests.add(test);
}
public enum FailureStrategy {
ALL_MUST_PASS,
AT_LEAST_ONE_MUST_PASS,
MAJORITY_MUST_PASS,
CUSTOM_THRESHOLD
}
}

6. Load Testing for Availability

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
public class LoadAvailabilityTest extends AvailabilityTest {
private final AvailabilityTest targetTest;
private final int concurrentUsers;
private final int totalRequests;
private final Duration rampUpPeriod;
public LoadAvailabilityTest(String testName, AvailabilityTest targetTest,
int concurrentUsers, int totalRequests, Duration rampUpPeriod) {
super(testName, Duration.ofMinutes(5)); // Extended timeout for load tests
this.targetTest = targetTest;
this.concurrentUsers = concurrentUsers;
this.totalRequests = totalRequests;
this.rampUpPeriod = rampUpPeriod;
}
@Override
public TestResult execute() {
Instant start = Instant.now();
AtomicInteger completedRequests = new AtomicInteger(0);
AtomicInteger successfulRequests = new AtomicInteger(0);
AtomicLong totalResponseTime = new AtomicLong(0);
List<TestResult> individualResults = Collections.synchronizedList(new ArrayList<>());
try {
// Create worker threads
List<CompletableFuture<Void>> workers = new ArrayList<>();
int requestsPerWorker = totalRequests / concurrentUsers;
for (int i = 0; i < concurrentUsers; i++) {
CompletableFuture<Void> worker = CompletableFuture.runAsync(() -> {
for (int j = 0; j < requestsPerWorker; j++) {
if (completedRequests.incrementAndGet() > totalRequests) {
break;
}
TestResult result = targetTest.execute();
individualResults.add(result);
if (result.isSuccess()) {
successfulRequests.incrementAndGet();
totalResponseTime.addAndGet(result.getDuration().toMillis());
}
// Simulate think time
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, scheduler);
workers.add(worker);
// Ramp up delay
if (i < concurrentUsers - 1) {
long rampUpDelay = rampUpPeriod.toMillis() / concurrentUsers;
Thread.sleep(rampUpDelay);
}
}
// Wait for all workers to complete
CompletableFuture<Void> allWorkers = CompletableFuture.allOf(
workers.toArray(new CompletableFuture[0]));
allWorkers.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
Duration duration = Duration.between(start, Instant.now());
return analyzeLoadResults(duration, successfulRequests.get(), 
completedRequests.get(), totalResponseTime.get(), 
individualResults);
} catch (TimeoutException e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.failure(testName, 
"Load test timed out after " + duration, duration);
} catch (Exception e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.error(testName, 
"Load test failed: " + e.getMessage(), duration, e);
}
}
private TestResult analyzeLoadResults(Duration duration, int successfulRequests,
int completedRequests, long totalResponseTime,
List<TestResult> individualResults) {
double successRate = (double) successfulRequests / completedRequests * 100;
double averageResponseTime = (double) totalResponseTime / successfulRequests;
double requestsPerSecond = (double) completedRequests / duration.getSeconds();
boolean success = successRate >= 95.0; // 95% success rate threshold
String message = String.format(
"Load test completed: %.2f%% success rate, %.2f req/sec, avg response: %.2f ms",
successRate, requestsPerSecond, averageResponseTime);
TestResult result = success ? 
TestResult.success(testName, message, duration) :
TestResult.failure(testName, message, duration);
// Add comprehensive metrics
result.addMetric("total_requests", completedRequests);
result.addMetric("successful_requests", successfulRequests);
result.addMetric("failed_requests", completedRequests - successfulRequests);
result.addMetric("success_rate_percent", successRate);
result.addMetric("requests_per_second", requestsPerSecond);
result.addMetric("average_response_time_ms", averageResponseTime);
result.addMetric("total_duration_seconds", duration.getSeconds());
result.addMetric("concurrent_users", concurrentUsers);
// Calculate percentiles
calculatePercentiles(individualResults, result);
return result;
}
private void calculatePercentiles(List<TestResult> results, TestResult aggregateResult) {
List<Long> responseTimes = results.stream()
.filter(TestResult::isSuccess)
.map(r -> r.getDuration().toMillis())
.sorted()
.collect(Collectors.toList());
if (!responseTimes.isEmpty()) {
aggregateResult.addMetric("p50_response_time_ms", 
percentile(responseTimes, 50));
aggregateResult.addMetric("p90_response_time_ms", 
percentile(responseTimes, 90));
aggregateResult.addMetric("p95_response_time_ms", 
percentile(responseTimes, 95));
aggregateResult.addMetric("p99_response_time_ms", 
percentile(responseTimes, 99));
}
}
private long percentile(List<Long> values, double percentile) {
int index = (int) Math.ceil(percentile / 100.0 * values.size());
return values.get(index - 1);
}
}

7. Failover and Recovery Testing

public class FailoverTest extends AvailabilityTest {
private final List<String> primaryEndpoints;
private final List<String> backupEndpoints;
private final Duration failoverTimeout;
private final int maxFailoverAttempts;
public FailoverTest(String testName, List<String> primaryEndpoints, 
List<String> backupEndpoints, Duration failoverTimeout) {
super(testName, Duration.ofMinutes(2));
this.primaryEndpoints = new ArrayList<>(primaryEndpoints);
this.backupEndpoints = new ArrayList<>(backupEndpoints);
this.failoverTimeout = failoverTimeout;
this.maxFailoverAttempts = 3;
}
@Override
public TestResult execute() {
Instant start = Instant.now();
try {
// Test primary endpoints
TestResult primaryResult = testEndpoints(primaryEndpoints, "primary");
if (primaryResult.isSuccess()) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.success(testName, 
"Primary endpoints are healthy", duration);
}
// Primary failed, test failover to backup
logger.warn("Primary endpoints failed, testing failover to backup endpoints");
TestResult failoverResult = performFailover();
Duration duration = Duration.between(start, Instant.now());
if (failoverResult.isSuccess()) {
return TestResult.success(testName, 
"Failover to backup endpoints successful", duration);
} else {
return TestResult.failure(testName, 
"Failover test failed: " + failoverResult.getMessage(), duration);
}
} catch (Exception e) {
Duration duration = Duration.between(start, Instant.now());
return TestResult.error(testName, 
"Failover test failed with error: " + e.getMessage(), duration, e);
}
}
private TestResult testEndpoints(List<String> endpoints, String endpointType) {
List<CompletableFuture<TestResult>> futures = endpoints.stream()
.map(endpoint -> CompletableFuture.supplyAsync(() -> 
new HttpEndpointTest(testName + "-" + endpointType, endpoint, 
Duration.ofSeconds(10)).execute(), scheduler))
.collect(Collectors.toList());
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0]));
try {
allFutures.get(failoverTimeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (Exception e) {
return TestResult.failure(testName, 
"Endpoint testing timed out for " + endpointType, Duration.ZERO);
}
// Check if any endpoint is healthy
boolean anyHealthy = futures.stream()
.map(CompletableFuture::join)
.anyMatch(TestResult::isSuccess);
if (anyHealthy) {
return TestResult.success(testName, 
"At least one " + endpointType + " endpoint is healthy", Duration.ZERO);
} else {
return TestResult.failure(testName, 
"All " + endpointType + " endpoints are unhealthy", Duration.ZERO);
}
}
private TestResult performFailover() {
for (int attempt = 1; attempt <= maxFailoverAttempts; attempt++) {
logger.info("Failover attempt {}/{}", attempt, maxFailoverAttempts);
TestResult backupResult = testEndpoints(backupEndpoints, "backup");
if (backupResult.isSuccess()) {
// Simulate failover process
if (simulateFailoverProcess()) {
return TestResult.success(testName, 
"Failover completed successfully on attempt " + attempt, Duration.ZERO);
}
}
// Wait before retry
if (attempt < maxFailoverAttempts) {
try {
Thread.sleep(5000); // 5 seconds between attempts
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
return TestResult.failure(testName, 
"All failover attempts failed", Duration.ZERO);
}
private boolean simulateFailoverProcess() {
// Simulate the actual failover process
// This could include:
// - Updating load balancer configuration
// - Switching DNS records
// - Promoting replica databases
// - etc.
try {
// Simulate failover time
Thread.sleep(2000);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}

8. Availability Test Runner and Scheduler

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
@Component
public class AvailabilityTestRunner {
private static final Logger logger = LoggerFactory.getLogger(AvailabilityTestRunner.class);
private final List<AvailabilityTest> tests = new CopyOnWriteArrayList<>();
private final Map<String, TestResult> latestResults = new ConcurrentHashMap<>();
private final Map<String, List<TestResult>> historicalResults = new ConcurrentHashMap<>();
private final int maxHistoryPerTest = 1000;
private final ScheduledExecutorService scheduler = 
Executors.newScheduledThreadPool(10);
public void registerTest(AvailabilityTest test) {
tests.add(test);
historicalResults.put(test.getTestName(), new ArrayList<>());
}
public void unregisterTest(String testName) {
tests.removeIf(test -> test.getTestName().equals(testName));
historicalResults.remove(testName);
latestResults.remove(testName);
}
@Scheduled(fixedRate = 60000) // Run every minute
public void runScheduledTests() {
logger.info("Running scheduled availability tests...");
tests.forEach(test -> {
CompletableFuture.runAsync(() -> {
try {
TestResult result = test.execute();
recordResult(test.getTestName(), result);
logger.info("Test {} completed: {}", test.getTestName(), 
result.isSuccess() ? "SUCCESS" : "FAILURE");
} catch (Exception e) {
logger.error("Test {} failed with exception: {}", test.getTestName(), e.getMessage());
}
}, scheduler);
});
}
public TestResult runTestImmediately(String testName) {
Optional<AvailabilityTest> test = tests.stream()
.filter(t -> t.getTestName().equals(testName))
.findFirst();
if (test.isPresent()) {
TestResult result = test.get().execute();
recordResult(testName, result);
return result;
} else {
throw new IllegalArgumentException("Test not found: " + testName);
}
}
public Map<String, TestResult> runAllTests() {
Map<String, TestResult> results = new HashMap<>();
tests.forEach(test -> {
TestResult result = test.execute();
recordResult(test.getTestName(), result);
results.put(test.getTestName(), result);
});
return results;
}
private void recordResult(String testName, TestResult result) {
latestResults.put(testName, result);
List<TestResult> history = historicalResults.computeIfAbsent(testName, 
k -> new ArrayList<>());
history.add(result);
// Trim history if needed
if (history.size() > maxHistoryPerTest) {
history.subList(0, history.size() - maxHistoryPerTest).clear();
}
}
public AvailabilityReport generateReport() {
return generateReport(Duration.ofHours(24));
}
public AvailabilityReport generateReport(Duration period) {
Instant cutoff = Instant.now().minus(period);
Map<String, TestStatistics> statistics = new HashMap<>();
for (Map.Entry<String, List<TestResult>> entry : historicalResults.entrySet()) {
String testName = entry.getKey();
List<TestResult> relevantResults = entry.getValue().stream()
.filter(result -> result.getTimestamp().isAfter(cutoff))
.collect(Collectors.toList());
if (!relevantResults.isEmpty()) {
statistics.put(testName, calculateStatistics(testName, relevantResults));
}
}
return new AvailabilityReport(Instant.now(), period, statistics);
}
private TestStatistics calculateStatistics(String testName, List<TestResult> results) {
long totalTests = results.size();
long successfulTests = results.stream().filter(TestResult::isSuccess).count();
double availability = (double) successfulTests / totalTests * 100;
List<Long> responseTimes = results.stream()
.filter(TestResult::isSuccess)
.map(r -> r.getDuration().toMillis())
.collect(Collectors.toList());
TestStatistics stats = new TestStatistics(testName, totalTests, successfulTests, availability);
if (!responseTimes.isEmpty()) {
Collections.sort(responseTimes);
stats.setAverageResponseTime(responseTimes.stream().mapToLong(Long::longValue).average().orElse(0));
stats.setP50ResponseTime(percentile(responseTimes, 50));
stats.setP95ResponseTime(percentile(responseTimes, 95));
stats.setP99ResponseTime(percentile(responseTimes, 99));
}
return stats;
}
private long percentile(List<Long> values, double percentile) {
int index = (int) Math.ceil(percentile / 100.0 * values.size());
return values.get(index - 1);
}
// Getters
public List<AvailabilityTest> getTests() { return new ArrayList<>(tests); }
public Map<String, TestResult> getLatestResults() { return new HashMap<>(latestResults); }
public List<TestResult> getTestHistory(String testName) { 
return new ArrayList<>(historicalResults.getOrDefault(testName, new ArrayList<>()));
}
@PreDestroy
public void shutdown() {
scheduler.shutdown();
tests.forEach(AvailabilityTest::shutdown);
}
}
class AvailabilityReport {
private final Instant generatedAt;
private final Duration period;
private final Map<String, TestStatistics> testStatistics;
private final double overallAvailability;
public AvailabilityReport(Instant generatedAt, Duration period, 
Map<String, TestStatistics> testStatistics) {
this.generatedAt = generatedAt;
this.period = period;
this.testStatistics = testStatistics;
this.overallAvailability = calculateOverallAvailability();
}
private double calculateOverallAvailability() {
if (testStatistics.isEmpty()) return 0.0;
double totalAvailability = testStatistics.values().stream()
.mapToDouble(TestStatistics::getAvailability)
.average()
.orElse(0.0);
return totalAvailability;
}
// Getters
public Instant getGeneratedAt() { return generatedAt; }
public Duration getPeriod() { return period; }
public Map<String, TestStatistics> getTestStatistics() { return testStatistics; }
public double getOverallAvailability() { return overallAvailability; }
}
class TestStatistics {
private final String testName;
private final long totalTests;
private final long successfulTests;
private final double availability;
private double averageResponseTime;
private long p50ResponseTime;
private long p95ResponseTime;
private long p99ResponseTime;
public TestStatistics(String testName, long totalTests, long successfulTests, double availability) {
this.testName = testName;
this.totalTests = totalTests;
this.successfulTests = successfulTests;
this.availability = availability;
}
// Getters and setters
public String getTestName() { return testName; }
public long getTotalTests() { return totalTests; }
public long getSuccessfulTests() { return successfulTests; }
public double getAvailability() { return availability; }
public double getAverageResponseTime() { return averageResponseTime; }
public void setAverageResponseTime(double averageResponseTime) { this.averageResponseTime = averageResponseTime; }
public long getP50ResponseTime() { return p50ResponseTime; }
public void setP50ResponseTime(long p50ResponseTime) { this.p50ResponseTime = p50ResponseTime; }
public long getP95ResponseTime() { return p95ResponseTime; }
public void setP95ResponseTime(long p95ResponseTime) { this.p95ResponseTime = p95ResponseTime; }
public long getP99ResponseTime() { return p99ResponseTime; }
public void setP99ResponseTime(long p99ResponseTime) { this.p99ResponseTime = p99ResponseTime; }
}

9. Spring Boot Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import javax.sql.DataSource;
@Configuration
@EnableScheduling
public class AvailabilityTestConfig {
@Bean
public AvailabilityTestRunner availabilityTestRunner() {
return new AvailabilityTestRunner();
}
@Bean
public HttpEndpointTest apiHealthTest() {
return new HttpEndpointTest(
"API Health Check",
"https://api.example.com/health",
Duration.ofSeconds(10)
);
}
@Bean
public HttpEndpointTest websiteAvailabilityTest() {
return new HttpEndpointTest(
"Website Availability",
"https://www.example.com",
Duration.ofSeconds(15),
200,
Map.of("Content-Type", "text/html"),
null
);
}
@Bean
public DatabaseConnectivityTest databaseTest(DataSource dataSource) {
return new DatabaseConnectivityTest(
"Database Connectivity",
dataSource,
Duration.ofSeconds(5)
);
}
@Bean
public CompositeAvailabilityTest fullSystemTest(
HttpEndpointTest apiHealthTest,
HttpEndpointTest websiteAvailabilityTest,
DatabaseConnectivityTest databaseTest) {
List<AvailabilityTest> tests = Arrays.asList(
apiHealthTest,
websiteAvailabilityTest,
databaseTest
);
return new CompositeAvailabilityTest(
"Full System Health Check",
tests,
CompositeAvailabilityTest.FailureStrategy.ALL_MUST_PASS,
Duration.ofSeconds(30)
);
}
@Bean
public LoadAvailabilityTest loadTest(HttpEndpointTest apiHealthTest) {
return new LoadAvailabilityTest(
"API Load Test",
apiHealthTest,
10,     // 10 concurrent users
100,    // 100 total requests
Duration.ofSeconds(30) // 30 second ramp-up
);
}
}

10. REST API for Test Management

import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/availability")
public class AvailabilityTestController {
private final AvailabilityTestRunner testRunner;
public AvailabilityTestController(AvailabilityTestRunner testRunner) {
this.testRunner = testRunner;
}
@GetMapping("/tests")
public List<Map<String, Object>> getRegisteredTests() {
return testRunner.getTests().stream()
.map(test -> Map.of(
"name", test.getTestName(),
"timeout", test.getTimeout().toString()
))
.collect(Collectors.toList());
}
@GetMapping("/results/latest")
public Map<String, AvailabilityTest.TestResult> getLatestResults() {
return testRunner.getLatestResults();
}
@GetMapping("/results/{testName}")
public List<AvailabilityTest.TestResult> getTestHistory(@PathVariable String testName) {
return testRunner.getTestHistory(testName);
}
@PostMapping("/tests/{testName}/run")
public AvailabilityTest.TestResult runTest(@PathVariable String testName) {
return testRunner.runTestImmediately(testName);
}
@PostMapping("/tests/run-all")
public Map<String, AvailabilityTest.TestResult> runAllTests() {
return testRunner.runAllTests();
}
@GetMapping("/report")
public AvailabilityReport getAvailabilityReport(
@RequestParam(defaultValue = "24h") String duration) {
Duration period = parseDuration(duration);
return testRunner.generateReport(period);
}
@PostMapping("/tests/register")
public String registerTest(@RequestBody TestRegistrationRequest request) {
AvailabilityTest test = createTestFromRequest(request);
testRunner.registerTest(test);
return "Test registered successfully: " + request.getName();
}
private Duration parseDuration(String duration) {
if (duration.endsWith("h")) {
long hours = Long.parseLong(duration.substring(0, duration.length() - 1));
return Duration.ofHours(hours);
} else if (duration.endsWith("d")) {
long days = Long.parseLong(duration.substring(0, duration.length() - 1));
return Duration.ofDays(days);
} else {
return Duration.ofHours(24); // Default to 24 hours
}
}
private AvailabilityTest createTestFromRequest(TestRegistrationRequest request) {
switch (request.getType()) {
case "HTTP":
return new HttpEndpointTest(
request.getName(),
request.getUrl(),
Duration.parse(request.getTimeout())
);
case "DATABASE":
// Would need DataSource injection
throw new UnsupportedOperationException("Database tests require DataSource");
default:
throw new IllegalArgumentException("Unsupported test type: " + request.getType());
}
}
public static class TestRegistrationRequest {
private String name;
private String type;
private String url;
private String timeout;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getTimeout() { return timeout; }
public void setTimeout(String timeout) { this.timeout = timeout; }
}
}

Usage Examples

@SpringBootApplication
public class AvailabilityTestingApplication {
public static void main(String[] args) {
SpringApplication.run(AvailabilityTestingApplication.class, args);
}
@Bean
public CommandLineRunner demo(AvailabilityTestRunner testRunner) {
return args -> {
// Run initial test suite
Map<String, AvailabilityTest.TestResult> results = testRunner.runAllTests();
results.forEach((testName, result) -> {
System.out.printf("Test %s: %s%n", testName, 
result.isSuccess() ? "PASSED" : "FAILED");
});
// Generate report
AvailabilityReport report = testRunner.generateReport();
System.out.printf("Overall availability: %.2f%%%n", report.getOverallAvailability());
};
}
}

This comprehensive availability testing framework provides:

  1. Multiple Test Types: HTTP, database, message queue, composite tests
  2. Load Testing: Concurrent user simulation and performance metrics
  3. Failover Testing: Automated failover and recovery testing
  4. Scheduled Execution: Regular automated testing
  5. Comprehensive Reporting: Availability metrics and historical data
  6. REST API: Programmatic test management and results retrieval
  7. Extensible Architecture: Easy to add new test types

The framework helps ensure system reliability by continuously monitoring availability and providing early warning of potential issues.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper