CPTS_581_DELIVERABLE2_JORDAN - WSU-CptS-581-2025/zerocode GitHub Wiki

Deliverable 2 Refactors - Jordan Liebe

This documentation reflects the two improvements that I made for my Second Deliverable for CPT_S 581.

Improvement #1

Centralized getMainModuleInjector in RunnerUtils.java

Reasoning for Change

In the code itself there was actually a suggested refactor that spurred my interest. Someone had already noticed that the getMainModuleInjector() function was being duplicated into both the Unit Runner and the Package Runner. Using that helpful context, I got to work investigating how the getMainModuleInjector() function worked.

Code Changes

As you can see in the below examples, both

ZeroCodePackageRunner.java version

// This is exact duplicate of ZeroCodeUnitRunner.getMainModuleInjector
// Refactor and maintain a single method in RunnerUtils
public Injector getMainModuleInjector() {
    //TODO: Synchronise this with e.g. synchronized (ZeroCodePackageRunner.class) {}
    final TargetEnv envAnnotation = testClass.getAnnotation(TargetEnv.class);
    String serverEnv = envAnnotation != null ? envAnnotation.value() : "config_hosts.properties";

    serverEnv = getEnvSpecificConfigFile(serverEnv, testClass);

    Class<? extends BasicHttpClient> runtimeHttpClient = createCustomHttpClientOrDefault();
    Class<? extends BasicKafkaClient> runtimeKafkaClient = createCustomKafkaClientOrDefault();

    return createInjector(Modules.override(new ApplicationMainModule(serverEnv))
            .with(
                    new RuntimeHttpClientModule(runtimeHttpClient),
                    new RuntimeKafkaClientModule(runtimeKafkaClient)
            ));
}

ZeroCodeUnitRunner.java version

public Injector getMainModuleInjector() {
    // Synchronise this with an object lock e.g. synchronized (ZeroCodeUnitRunner.class) {}
    synchronized (this) {
        final TargetEnv envAnnotation = testClass.getAnnotation(TargetEnv.class);
        String serverEnv = envAnnotation != null ? envAnnotation.value() : "config_hosts.properties";

        serverEnv = getEnvSpecificConfigFile(serverEnv, testClass);

        Class<? extends BasicHttpClient> runtimeHttpClient = createCustomHttpClientOrDefault();
        Class<? extends BasicKafkaClient> runtimeKafkaClient = createCustomKafkaClientOrDefault();

        injector = Guice.createInjector(Modules.override(new ApplicationMainModule(serverEnv))
                .with(
                        new RuntimeHttpClientModule(runtimeHttpClient),
                        new RuntimeKafkaClientModule(runtimeKafkaClient)
                )
        );

        return injector;
    }
}

After reading through the notes on both functions, it appears that someone intended to also make the ZeroCodeUnitRunner synchronized, so I made sure my new version in RunnerUtils.java was synchronized.

Here is my finalized function that was added to RunnerUtils.java.

public static Injector getMainModuleInjector(Class<?> testClass) {
    synchronized (testClass) {
        // Retrieve the TargetEnv annotation
        final TargetEnv envAnnotation = testClass.getAnnotation(TargetEnv.class);
        String serverEnv = envAnnotation != null ? envAnnotation.value() : "config_hosts.properties";

        // Resolve environment-specific configuration file
        serverEnv = getEnvSpecificConfigFile(serverEnv, testClass);

        // Determine the runtime HTTP and Kafka clients
        Class<? extends BasicHttpClient> runtimeHttpClient = createCustomHttpClientOrDefault(testClass);
        Class<? extends BasicKafkaClient> runtimeKafkaClient = createCustomKafkaClientOrDefault(testClass);

        // Create and return the Guice injector
        return Guice.createInjector(Modules.override(new ApplicationMainModule(serverEnv))
            .with(
                    new RuntimeHttpClientModule(runtimeHttpClient),
                    new RuntimeKafkaClientModule(runtimeKafkaClient)
            ));
    }
}

One minor issue I ran into along the way was finding out how to handle the testClass parameter. When I first worked on this, I read the Class<?> type as a generic type like in C++, but in reality it acts more like a Type object in C#. Once I figured that out I was able to figure the rest out pretty quickly.

There were also several other functions that this function depended on that got shuffled over to RunnerUtils.java in order for this function to work, but also because they were similarly identical in both files.

createCustomHttpClientOrDefault() - Used by getMainModuleInjector()

public Class<? extends BasicHttpClient> createCustomHttpClientOrDefault() {
    final UseHttpClient httpClientAnnotated = getUseHttpClient();
    return httpClientAnnotated != null ? httpClientAnnotated.value() : SslTrustHttpClient.class;
}

createCustomKafkaClientOrDefault() - Used by getMainModuleInjector()

public Class<? extends BasicKafkaClient> createCustomKafkaClientOrDefault() {
    final UseKafkaClient kafkaClientAnnotated = getUseKafkaClient();
    return kafkaClientAnnotated != null ? kafkaClientAnnotated.value() : ZerocodeCustomKafkaClient.class;
}

getUseHttpClient() - Used by createCustomHttpClientOrDefault()

public UseHttpClient getUseHttpClient() {
    return testClass.getAnnotation(UseHttpClient.class);
}

getUseKafkaClient() - Used by createCustomKafkaClientOrDefault()

public UseKafkaClient getUseKafkaClient() {
    return testClass.getAnnotation(UseKafkaClient.class);
}

Improvement #2

Split up the generateExtentReport() function to make it more readable and expandable

Reasoning for Change

When searching for another opportunity for refactoring near my RunnerUtils.java change, I kind of just stumbled across this reporting functionality that was pretty complex. This entire function was obviously well over the 15 line recommendation past CS Professors have given me but it also just packed a ton of logic all into one chunk of code. So following my gut I thought this needed to change and be broken out into smaller more manageable functions, that were also more readable, understandable, and extendable.

Code Changes

Original Report Generation Function

@Override
public void generateExtentReport() {

@@ -95,57 +99,107 @@ public void generateExtentReport() {
    linkToSpikeChartIfEnabled();

    treeReports.forEach(thisReport -> {

        thisReport.getResults().forEach(thisScenario -> {
            ExtentTest test = extentReports.createTest(thisScenario.getScenarioName());

                /**This code checks if the scenario has meta data.
                If it does, it iterates through each meta data entry and adds it to
                the Extent report as an info label.**/
            if (thisScenario.getMeta() != null) {
                for (Map.Entry<String, List<String>> entry : thisScenario.getMeta().entrySet()) {
                    String key = entry.getKey();
                    List<String> values = entry.getValue();
                    test.info(MarkupHelper.createLabel(key + ": " + String.join(", ", values), ExtentColor.BLUE));
                }
            }

            // Assign Category
            test.assignCategory(DEFAULT_REGRESSION_CATEGORY); //Super set
            String[] hashTagsArray = optionalCategories(thisScenario.getScenarioName()).toArray(new String[0]);
            if(hashTagsArray.length > 0) {
                test.assignCategory(hashTagsArray); //Sub categories
            }

            // Assign Authors
            test.assignAuthor(DEFAULT_REGRESSION_AUTHOR); //Super set
            String[] authorsArray = optionalAuthors(thisScenario.getScenarioName()).toArray(new String[0]);
            if(authorsArray.length > 0) {
                test.assignAuthor(authorsArray); //Sub authors
            }

            List<ZeroCodeReportStep> thisScenarioUniqueSteps = getUniqueSteps(thisScenario.getSteps());
            thisScenarioUniqueSteps.forEach(thisStep -> {
                test.getModel().setStartTime(utilDateOf(thisStep.getRequestTimeStamp()));
                test.getModel().setEndTime(utilDateOf(thisStep.getResponseTimeStamp()));

                final Status testStatus = thisStep.getResult().equals(RESULT_PASS) ? Status.PASS : Status.FAIL;

                ExtentTest step = test.createNode(thisStep.getName(), TEST_STEP_CORRELATION_ID + " " + thisStep.getCorrelationId());

                if (testStatus.equals(Status.PASS)) {
                    step.pass(thisStep.getResult());
                } else {
                    step.info(MarkupHelper.createCodeBlock(thisStep.getOperation() + "\t" + thisStep.getUrl()));
                    step.info(MarkupHelper.createCodeBlock(thisStep.getRequest(), CodeLanguage.JSON));
                    step.info(MarkupHelper.createCodeBlock(thisStep.getResponse(), CodeLanguage.JSON));
                    step.fail(MarkupHelper.createCodeBlock("Reason:\n" + thisStep.getAssertions()));
                }
                extentReports.flush();
            });

        });

    });
}

In this code, there is already double loop occuring, so my first improvement was to break the individual report processing into its own function.

/**
    * Processes the report and generates the Extent report. See referenced functions for detailed steps.
    *
    * @param report        The ZeroCodeReport object representing the test report.
    * @param extentReports The ExtentReports object used to generate the report.
    */
protected void processReport(ZeroCodeReport report, ExtentReports extentReports) {
    report.getResults().forEach(thisScenario -> {
        ExtentTest test = extentReports.createTest(thisScenario.getScenarioName());

        // Processes any metadata if present
        processMetaData(test, thisScenario);

        // Assign Category
        assignCategory(test, thisScenario);

        // Assign Authors
        assignAuthors(test, thisScenario);

        // Extract the individual test steps
        extractSteps(test, thisScenario, extentReports);
    });
}

Then, as you may notice by the body of that function I broke the individual report generation out into 4 distinct steps: Meta Data Processing, Category Assigning, Author Assigning, and Step Extraction.

Metadata processing was pretty straight forward as it just iterated through a key/value pair object and mapped them into the report object. It looked like this:

/**
 * Processes the metadata of a scenario and adds it to the Extent report.
 * @param test
 * @param scenario
 */
protected void processMetaData(ExtentTest test, ZeroCodeExecResult scenario) {
    /**This code checks if the scenario has meta data.
            If it does, it iterates through each meta data entry and adds it to
            the Extent report as an info label.**/
    if (scenario.getMeta() != null) {
        for (Map.Entry<String, List<String>> entry : scenario.getMeta().entrySet()) {
            String key = entry.getKey();
            List<String> values = entry.getValue();
            test.info(MarkupHelper.createLabel(key + ": " + String.join(", ", values), ExtentColor.BLUE));
        }
    }
}

Then Assigning Categories looked like this:

/**
 * Assigns categories to the test case in the Extent report.
 * @param test
 * @param scenario
 */
protected void assignCategory(ExtentTest test, ZeroCodeExecResult scenario) {
    // Assign Category
    test.assignCategory(DEFAULT_REGRESSION_CATEGORY); //Super set
    String[] hashTagsArray = optionalCategories(scenario.getScenarioName()).toArray(new String[0]);
    if(hashTagsArray.length > 0) {
        test.assignCategory(hashTagsArray); //Sub categories
    }
}

Assigning Authors looked like this:

/**
 * Assigns authors to the test case in the Extent report.
 * @param test
 * @param scenario
 */
protected void assignAuthors(ExtentTest test, ZeroCodeExecResult scenario) {
    // Assign Authors
    test.assignAuthor(DEFAULT_REGRESSION_AUTHOR); //Super set
    String[] authorsArray = optionalAuthors(scenario.getScenarioName()).toArray(new String[0]);
    if(authorsArray.length > 0) {
        test.assignAuthor(authorsArray); //Sub authors
    }
}

And finally, Extracting Steps looked like:

/**
 * Extracts the steps from the scenario and creates a node for each step in the Extent report.
 * It also sets the start and end time for each step based on the request and response timestamps.
 *
 * @param test              The ExtentTest object representing the test case.
 * @param scenario          The ZeroCodeExecResult object representing the scenario.
 * @param extentReports     The ExtentReports object used to generate the report.
 */
protected void extractSteps(ExtentTest test, ZeroCodeExecResult scenario, ExtentReports extentReports) {
    List<ZeroCodeReportStep> thisScenarioUniqueSteps = getUniqueSteps(scenario.getSteps());
    thisScenarioUniqueSteps.forEach(thisStep -> {
        test.getModel().setStartTime(utilDateOf(thisStep.getRequestTimeStamp()));
        test.getModel().setEndTime(utilDateOf(thisStep.getResponseTimeStamp()));

        final Status testStatus = thisStep.getResult().equals(RESULT_PASS) ? Status.PASS : Status.FAIL;

        ExtentTest step = test.createNode(thisStep.getName(), TEST_STEP_CORRELATION_ID + " " + thisStep.getCorrelationId());

        if (testStatus.equals(Status.PASS)) {
            step.pass(thisStep.getResult());
        } else {
            step.info(MarkupHelper.createCodeBlock(thisStep.getOperation() + "\t" + thisStep.getUrl()));
            step.info(MarkupHelper.createCodeBlock(thisStep.getRequest(), CodeLanguage.JSON));
            step.info(MarkupHelper.createCodeBlock(thisStep.getResponse(), CodeLanguage.JSON));
            step.fail(MarkupHelper.createCodeBlock("Reason:\n" + thisStep.getAssertions()));
        }
        extentReports.flush();
    });
}

Then after building out these new simpler functions, I was able to simplify the main public method down to:

/**
 * Generates the Extent report. It creates a new ExtentReports object and processes each report
 * in the treeReports list to generate the report.
 */
@Override
public void generateExtentReport() {
    linkToSpikeChartIfEnabled();

    treeReports.forEach(thisReport -> {
        processReport(thisReport, extentReports);
    });
}
⚠️ **GitHub.com Fallback** ⚠️