This document provides specific technical implementation details for enhancing the API Automation Framework into a world-class solution.
Note: For framework analysis and roadmap overview, see Framework Analysis & Enhancement Roadmap.
// Base framework exception
public abstract class FrameworkException extends RuntimeException {
private final ErrorCode errorCode;
private final Map<String, Object> context;
private final LocalDateTime timestamp;
public FrameworkException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
this.context = new HashMap<>();
this.timestamp = LocalDateTime.now();
}
public void addContext(String key, Object value) {
this.context.put(key, value);
}
}
// Specific exception types
public class ConfigurationException extends FrameworkException {
public ConfigurationException(String message) {
super(ErrorCode.CONFIGURATION_ERROR, message);
}
}
public class ValidationException extends FrameworkException {
public ValidationException(String message) {
super(ErrorCode.VALIDATION_ERROR, message);
}
}
// Error codes enum
public enum ErrorCode {
CONFIGURATION_ERROR("CFG-001", "Configuration error"),
VALIDATION_ERROR("VAL-001", "Validation error"),
REQUEST_ERROR("REQ-001", "Request error");
private final String code;
private final String description;
}
@Retry(maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
public class RetryManager {
@Retryable(value = {RequestException.class}, maxAttempts = 3)
public Response executeWithRetry(Supplier<Response> requestSupplier) {
try {
return requestSupplier.get();
} catch (RequestException e) {
logger.warn("Request failed, attempt {} of {}", getCurrentAttempt(), getMaxAttempts());
throw e; // Will trigger retry
}
}
}
// Usage
@Retry(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public Response makeRequestWithRetry() {
return RetryManager.executeWithRetry(() -> UserApi.login(loginRequest));
}
@ConfigurationSource("system")
@ConfigurationSource("environment")
@ConfigurationSource("file:config.properties")
@ConfigurationSource("file:environment/${env}.properties")
public interface ConfigFactory {
@ConfigProperty("BASE_URL")
@DefaultValue("http://localhost:8080")
@Validation(required = true, url = true)
String getBaseUrl();
@ConfigProperty("TIMEOUT")
@DefaultValue("30000")
@Validation(min = 1000, max = 300000)
int getTimeout();
@ConfigProperty("AUTH_TOKEN")
@Sensitive
@Validation(required = true)
String getAuthToken();
}
// Configuration validation
@ConfigurationValidator
public class ConfigValidator {
@PostConstruct
public void validateConfiguration() {
validateRequiredProperties();
validatePropertyTypes();
validatePropertyRanges();
}
}
public interface FrameworkPlugin {
String getName();
String getVersion();
String getDescription();
List<String> getDependencies();
void initialize(FrameworkContext context);
void shutdown();
}
// Plugin context
public class FrameworkContext {
private final Map<String, Object> registry = new ConcurrentHashMap<>();
public void registerService(String name, Object service) {
registry.put(name, service);
}
public <T> T getService(String name, Class<T> type) {
return type.cast(registry.get(name));
}
public void registerValidator(String name, ResponseValidator validator) {
registry.put("validator." + name, validator);
}
}
@FrameworkPlugin(
name = "custom-validation",
version = "1.0.0",
description = "Custom validation plugin",
dependencies = {"core-validation"}
)
public class CustomValidationPlugin implements FrameworkPlugin {
@Override
public void initialize(FrameworkContext context) {
// Register custom validators
context.registerValidator("email-format", new EmailValidator());
context.registerValidator("phone-format", new PhoneValidator());
}
@Override
public void shutdown() {
logger.info("Custom validation plugin shutting down");
}
}
@TestDataFactory
public class UserTestDataFactory {
@TestData("valid-user")
public UserLoginRequest createValidUser() {
return new UserLoginRequest("testuser", "testpass");
}
@TestData("admin-user")
public UserLoginRequest createAdminUser() {
return new UserLoginRequest("admin", "admin123");
}
@TestData("random-user")
public UserLoginRequest createRandomUser() {
String username = "user_" + System.currentTimeMillis();
String password = generateRandomPassword();
return new UserLoginRequest(username, password);
}
private String generateRandomPassword() {
return RandomStringUtils.randomAlphanumeric(8);
}
}
@TestDataManager
public class TestDataManager {
private final Map<String, Object> testData = new ConcurrentHashMap<>();
private final List<TestDataCleanup> cleanupTasks = new ArrayList<>();
@BeforeTest
public void setupTestData() {
loadTestDataFactories();
createTestData();
}
@AfterTest
public void cleanupTestData() {
for (int i = cleanupTasks.size() - 1; i >= 0; i--) {
try {
cleanupTasks.get(i).cleanup();
} catch (Exception e) {
logger.error("Failed to cleanup test data", e);
}
}
testData.clear();
cleanupTasks.clear();
}
public <T> T getTestData(String name, Class<T> type) {
return type.cast(testData.get(name));
}
public void registerCleanup(TestDataCleanup cleanup) {
cleanupTasks.add(cleanup);
}
}
// Test data cleanup interface
public interface TestDataCleanup {
void cleanup() throws Exception;
}
@Validator("email-format")
public class EmailValidator implements ResponseValidator {
private static final String EMAIL_PATTERN =
"^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
@Override
public ValidationResult validate(Response response, String field) {
String email = response.jsonPath().getString(field);
if (email == null) {
return ValidationResult.failure("Email field is null");
}
boolean isValid = Pattern.matches(EMAIL_PATTERN, email);
return ValidationResult.of(isValid,
isValid ? "Email format is valid" : "Email format is invalid: " + email);
}
}
@Validator("positive-number")
public class PositiveNumberValidator implements ResponseValidator {
@Override
public ValidationResult validate(Response response, String field) {
Object value = response.jsonPath().get(field);
if (value == null) {
return ValidationResult.failure("Field is null");
}
try {
double number = Double.parseDouble(value.toString());
boolean isValid = number > 0;
return ValidationResult.of(isValid,
isValid ? "Number is positive" : "Number is not positive: " + number);
} catch (NumberFormatException e) {
return ValidationResult.failure("Field is not a number: " + value);
}
}
}
public class ValidationChain {
private final List<ValidationStep> steps = new ArrayList<>();
private final List<ValidationResult> results = new ArrayList<>();
public ValidationChain addValidator(ResponseValidator validator, String field) {
steps.add(new ValidationStep(validator, field));
return this;
}
public ValidationChain addCondition(Predicate<Response> condition, String description) {
steps.add(new ValidationStep(null, condition, description));
return this;
}
public ValidationResult validate(Response response) {
results.clear();
for (ValidationStep step : steps) {
ValidationResult result = step.execute(response);
results.add(result);
if (!result.isSuccess() && step.isStopOnFailure()) {
break;
}
}
boolean overallSuccess = results.stream().allMatch(ValidationResult::isSuccess);
String message = overallSuccess ? "All validations passed" :
"Validation failed: " + results.stream()
.filter(r -> !r.isSuccess())
.map(ValidationResult::getMessage)
.collect(Collectors.joining(", "));
return ValidationResult.of(overallSuccess, message, results);
}
}
// Usage
ValidationChain chain = new ValidationChain()
.addCondition(r -> r.getStatusCode() == 200, "Status code should be 200")
.addValidator(new EmailValidator(), "data.email")
.addValidator(new PositiveNumberValidator(), "data.age")
.addCondition(r -> r.getTime() < 5000, "Response time should be under 5 seconds");
ValidationResult result = chain.validate(response);
if (!result.isSuccess()) {
throw new ValidationException(result.getMessage());
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitored {
String name() default "";
String category() default "default";
boolean logMetrics() default true;
boolean recordToDatabase() default false;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PerformanceThreshold {
long maxDuration() default 5000;
boolean failOnViolation() default false;
}
@Aspect
@Component
public class PerformanceMonitor {
private final MetricsCollector metricsCollector;
private final Logger logger = LoggerFactory.getLogger(PerformanceMonitor.class);
@Around("@annotation(monitored)")
public Object monitorPerformance(ProceedingJoinPoint joinPoint, Monitored monitored) {
String methodName = monitored.name().isEmpty() ?
joinPoint.getSignature().getName() : monitored.name();
String category = monitored.category();
long startTime = System.currentTimeMillis();
long startNanoTime = System.nanoTime();
try {
Object result = joinPoint.proceed();
recordSuccess(methodName, category, startTime, startNanoTime, monitored);
return result;
} catch (Throwable e) {
recordFailure(methodName, category, startTime, startNanoTime, e, monitored);
throw e;
}
}
@Around("@annotation(threshold)")
public Object checkPerformanceThreshold(ProceedingJoinPoint joinPoint, PerformanceThreshold threshold) {
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
if (duration > threshold.maxDuration()) {
String message = String.format("Performance threshold violated: %dms > %dms",
duration, threshold.maxDuration());
if (threshold.failOnViolation()) {
throw new PerformanceException(message);
} else {
logger.warn(message);
}
}
return result;
} catch (Throwable e) {
if (e instanceof PerformanceException) {
throw e;
}
long duration = System.currentTimeMillis() - startTime;
logger.error("Method execution failed after {}ms", duration, e);
throw e;
}
}
private void recordSuccess(String methodName, String category, long startTime,
long startNanoTime, Monitored monitored) {
long duration = System.currentTimeMillis() - startTime;
long nanoDuration = System.nanoTime() - startNanoTime;
if (monitored.logMetrics()) {
logger.info("Method {} completed in {}ms ({}ns)", methodName, duration, nanoDuration);
}
if (monitored.recordToDatabase()) {
metricsCollector.recordSuccess(methodName, category, duration, nanoDuration);
}
}
}
// Usage
@Monitored(name = "user-login", category = "authentication", logMetrics = true)
@PerformanceThreshold(maxDuration = 3000, failOnViolation = true)
public Response login(UserLoginRequest request) {
return UserApi.login(request);
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Secure {
String[] requiredRoles() default {};
String[] requiredPermissions() default {};
boolean requireAuthentication() default true;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveData {
String[] fields() default {};
String[] headers() default {};
String maskingStrategy() default "default";
}
@Aspect
@Component
public class SecurityInterceptor {
private final SecurityManager securityManager;
private final SensitiveDataHandler sensitiveDataHandler;
@Before("@annotation(secure)")
public void validateSecurity(JoinPoint joinPoint, Secure secure) {
// Validate authentication
if (secure.requireAuthentication() && !securityManager.isAuthenticated()) {
throw new SecurityException("Authentication required");
}
// Validate roles
if (secure.requiredRoles().length > 0) {
String[] userRoles = securityManager.getUserRoles();
boolean hasRequiredRole = Arrays.stream(secure.requiredRoles())
.anyMatch(role -> Arrays.asList(userRoles).contains(role));
if (!hasRequiredRole) {
throw new SecurityException("Insufficient roles");
}
}
}
@AfterReturning("@annotation(sensitiveData)")
public void handleSensitiveData(JoinPoint joinPoint, SensitiveData sensitiveData, Object result) {
if (result instanceof Response) {
Response response = (Response) result;
sensitiveDataHandler.maskSensitiveData(response, sensitiveData);
}
}
}
// Usage
@Secure(requiredRoles = {"admin"}, requireAuthentication = true)
@SensitiveData(fields = {"password", "ssn"}, maskingStrategy = "hash")
public Response createUser(UserCreateRequest request) {
return UserApi.createUser(request);
}
@ReportGenerator("html")
public class HtmlReportGenerator implements ReportGenerator {
@Override
public void generateReport(TestExecutionResult result) {
HtmlReport report = new HtmlReport();
report.setTitle("API Test Execution Report");
report.setExecutionTime(result.getExecutionTime());
report.setTotalTests(result.getTotalTests());
report.setPassedTests(result.getPassedTests());
report.setFailedTests(result.getFailedTests());
// Add test details
for (TestResult testResult : result.getTestResults()) {
report.addTestResult(testResult);
}
// Generate HTML file
String reportPath = "target/reports/api-test-report.html";
report.generate(reportPath);
logger.info("HTML report generated: {}", reportPath);
}
}
@ReportGenerator("json")
public class JsonReportGenerator implements ReportGenerator {
private final ObjectMapper objectMapper;
public JsonReportGenerator(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public void generateReport(TestExecutionResult result) {
try {
String reportPath = "target/reports/api-test-report.json";
objectMapper.writeValue(new File(reportPath), result);
logger.info("JSON report generated: {}", reportPath);
} catch (IOException e) {
logger.error("Failed to generate JSON report", e);
}
}
}
@Component
public class AnalyticsCollector {
private final MetricsRepository metricsRepository;
private final PerformanceRepository performanceRepository;
public void collectTestMetrics(TestExecutionResult result) {
TestMetrics metrics = TestMetrics.builder()
.executionId(result.getExecutionId())
.totalTests(result.getTotalTests())
.passedTests(result.getPassedTests())
.failedTests(result.getFailedTests())
.executionTime(result.getExecutionTime())
.timestamp(LocalDateTime.now())
.build();
metricsRepository.save(metrics);
}
public void collectPerformanceMetrics(PerformanceMetrics metrics) {
performanceRepository.save(metrics);
}
public AnalyticsReport generateAnalyticsReport(LocalDateTime startDate, LocalDateTime endDate) {
List<TestMetrics> testMetrics = metricsRepository.findByTimestampBetween(startDate, endDate);
List<PerformanceMetrics> performanceMetrics = performanceRepository.findByTimestampBetween(startDate, endDate);
return AnalyticsReport.builder()
.period(new DateRange(startDate, endDate))
.testMetrics(testMetrics)
.performanceMetrics(performanceMetrics)
.trends(calculateTrends(testMetrics))
.recommendations(generateRecommendations(testMetrics, performanceMetrics))
.build();
}
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CICDReady {
String[] environments() default {"dev", "staging", "prod"};
boolean parallelExecution() default true;
int maxParallelThreads() default 4;
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnvironmentAware {
String[] environments() default {};
String[] excludedEnvironments() default {};
}
@Component
public class CICDUtils {
public void setupCIEnvironment() {
// Set up CI-specific environment variables
System.setProperty("ci.environment", "true");
System.setProperty("ci.build.number", getBuildNumber());
System.setProperty("ci.build.url", getBuildUrl());
// Configure logging for CI
configureCILogging();
// Set up test data for CI
setupCITestData();
}
public void generateCIReport(TestExecutionResult result) {
// Generate CI-specific reports
generateJUnitReport(result);
generateCoverageReport(result);
generatePerformanceReport(result);
// Upload reports to CI system
uploadReportsToCI();
}
public void notifySlack(TestExecutionResult result) {
SlackMessage message = SlackMessage.builder()
.channel("#test-automation")
.text("API Test Execution Complete")
.attachment(createTestResultAttachment(result))
.build();
slackService.sendMessage(message);
}
private SlackAttachment createTestResultAttachment(TestExecutionResult result) {
String color = result.getFailedTests() > 0 ? "danger" : "good";
return SlackAttachment.builder()
.color(color)
.title("Test Results")
.field("Total Tests", String.valueOf(result.getTotalTests()), true)
.field("Passed", String.valueOf(result.getPassedTests()), true)
.field("Failed", String.valueOf(result.getFailedTests()), true)
.field("Execution Time", formatDuration(result.getExecutionTime()), true)
.build();
}
}
This technical enhancement guide provides a comprehensive roadmap for transforming the current framework into a world-class, enterprise-ready API automation solution. Each enhancement builds upon the previous ones, creating a robust and extensible framework that can compete with commercial solutions while remaining open source and community-driven.