All Downloads are FREE. Search and download functionalities are using the official Maven repository.

es.iti.wakamiti.junit.WakamitiJUnitRunner Maven / Gradle / Ivy

/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */
package es.iti.wakamiti.junit;


import es.iti.wakamiti.api.BackendFactory;
import es.iti.wakamiti.api.WakamitiConfiguration;
import es.iti.wakamiti.api.WakamitiException;
import es.iti.wakamiti.api.event.Event;
import es.iti.wakamiti.api.plan.PlanNode;
import es.iti.wakamiti.api.plan.PlanNodeSnapshot;
import es.iti.wakamiti.core.Wakamiti;
import es.iti.wakamiti.core.runner.PlanNodeLogger;
import es.iti.wakamiti.api.imconfig.Configuration;
import es.iti.wakamiti.api.imconfig.ConfigurationException;
import es.iti.wakamiti.api.imconfig.ConfigurationFactory;
import org.junit.*;
import org.junit.internal.runners.statements.RunAfters;
import org.junit.internal.runners.statements.RunBefores;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import static es.iti.wakamiti.api.WakamitiConfiguration.*;


/**
 * JUnit Runner for executing Wakamiti plan.
 *
 * 

This custom JUnit runner integrates Wakamiti plan nodes with JUnit for test execution. It extends * {@link ParentRunner} and manages the execution of Wakamiti test plan nodes within a JUnit framework. The * runner supports the execution of test suites and test cases, providing descriptions and handling child nodes * accordingly.

* *

The runner ensures proper initialization and finalization of the Wakamiti framework, captures test plan * results, and supports the configuration of Wakamiti settings through annotations on the test class.

* *

Annotations such as {@link BeforeClass}, {@link AfterClass}, and {@link Test} are not allowed on the * test class, as Wakamiti manages its own lifecycle and execution flow.

* * @author María Galbis Calomarde - [email protected] */ public class WakamitiJUnitRunner extends ParentRunner { protected static final Logger LOGGER = es.iti.wakamiti.core.Wakamiti.LOGGER; protected static final ConfigurationFactory CONF_BUILDER = ConfigurationFactory.instance(); protected final PlanNodeLogger planNodeLogger; protected final boolean treatStepsAsTests; protected final es.iti.wakamiti.core.Wakamiti wakamiti; protected final Configuration configuration; private final PlanNode plan; private List children; /** * Constructs a WakamitiJUnitRunner for the specified test class. * *

This constructor initializes the Wakamiti framework, creates a test plan based on the configuration, * and configures the Wakamiti logger and event observers.

* * @param configurationClass The test class containing the Wakamiti configuration annotations. * @throws InitializationError If there is an error initializing the runner. */ public WakamitiJUnitRunner(Class configurationClass) throws InitializationError { super(configurationClass); this.wakamiti = es.iti.wakamiti.core.Wakamiti.instance(); this.configuration = retrieveConfiguration(configurationClass); this.plan = wakamiti.createPlanFromConfiguration(configuration); this.planNodeLogger = new PlanNodeLogger(LOGGER, configuration, plan); this.treatStepsAsTests = configuration.get(TREAT_STEPS_AS_TESTS, Boolean.class).orElse(Boolean.FALSE); } /** * Retrieves the Wakamiti configuration for the specified test class. * *

This method is responsible for fetching the Wakamiti configuration for a given test class. * It uses Wakamiti's default configuration and appends additional configuration obtained from * annotations present on the test class. If there is an error loading the configuration, * it logs an error message and throws an InitializationError.

* * @param testedClass The test class for which to retrieve the configuration. * @return The Wakamiti configuration for the specified test class. * @throws InitializationError If an error occurs during configuration retrieval. */ private Configuration retrieveConfiguration(Class testedClass) throws InitializationError { try { Configuration config = es.iti.wakamiti.core.Wakamiti.defaultConfiguration(); Optional altDir = Optional.ofNullable(getClass().getClassLoader().getResource(".")) .map(u -> { try { return u.toURI(); } catch (URISyntaxException e) { return null; } }) .map(url -> Path.of(url).toString().replace(System.getProperty("user.dir"), "")) .map(dir -> dir.replaceAll("^[\\\\/]([^\\\\/]+).*", "$1")); if (altDir.isPresent()) { config = config.appendFromPairs( OUTPUT_FILE_PATH, String.format("%s/%s", altDir.get(), WakamitiConfiguration.DEFAULTS.get(OUTPUT_FILE_PATH, String.class) .orElse("wakamiti.json")), OUTPUT_FILE_PER_TEST_CASE_PATH, altDir.get()); } return config.appendFromAnnotation(testedClass); } catch (ConfigurationException e) { LOGGER.error("Error loading configuration from {}", testedClass); throw new InitializationError(e); } } /** * Retrieves the child nodes representing test suites or test cases. * *

This method creates the child runners based on the Wakamiti plan and configuration settings.

* * @return A list of PlanNodeJUnitRunner instances representing the child nodes. */ @Override protected List getChildren() { if (children == null) { children = createChildren(); } return children; } /** * Describes a child node for reporting purposes. * *

This method returns a JUnit Description for the specified child runner.

* * @param child The child runner. * @return A Description object representing the child node. */ @Override protected Description describeChild(PlanNodeJUnitRunner child) { return child.getDescription(); } /** * Runs a child node with the provided RunNotifier. * *

This method executes the specified child runner, capturing the results using the provided RunNotifier.

* * @param child The child runner representing a test suite or test case. * @param notifier The RunNotifier for reporting test execution events. */ @Override protected void runChild(PlanNodeJUnitRunner child, RunNotifier notifier) { child.run(notifier); } /** * Collects initialization errors for the test class. * *

This method validates that no annotated methods (e.g., BeforeClass, AfterClass, Test) are present in the * test class. Any violations are added to the list of errors.

* * @param errors The list to which validation errors are added. */ @Override protected void collectInitializationErrors(List errors) { super.collectInitializationErrors(errors); validateNoAnnotatedMethod(getTestClass().getJavaClass(), Before.class, errors); validateNoAnnotatedMethod(getTestClass().getJavaClass(), After.class, errors); validateNoAnnotatedMethod(getTestClass().getJavaClass(), Test.class, errors); } /** * Creates child runners for the test class. * * @return The list of child runners. */ protected List createChildren() { BackendFactory backendFactory = wakamiti.newBackendFactory(); return plan.children().map(node -> { Configuration featureConfiguration = configuration.append( CONF_BUILDER.fromMap(node.properties()) ); return treatStepsAsTests ? new PlanNodeStepJUnitRunner( node, featureConfiguration, backendFactory, planNodeLogger ) : new PlanNodeJUnitRunner(node, featureConfiguration, backendFactory, planNodeLogger); }).collect(Collectors.toList()); } /** * Initializes Wakamiti before the test plan execution. */ public void initWakamiti() { LOGGER.debug("{}", configuration); Wakamiti.contributors().propertyResolvers(configuration); wakamiti.configureLogger(configuration); wakamiti.configureEventObservers(configuration); plan.assignExecutionID(configuration.get(EXECUTION_ID, String.class).orElse(UUID.randomUUID().toString())); wakamiti.publishEvent(Event.PLAN_RUN_STARTED, new PlanNodeSnapshot(plan)); planNodeLogger.logTestPlanHeader(plan); } /** * Finalizes Wakamiti after the test plan execution. */ public void finalizeWakamiti() { planNodeLogger.logTestPlanResult(plan); var snapshot = new PlanNodeSnapshot(plan); wakamiti.publishEvent(Event.PLAN_RUN_FINISHED, snapshot); wakamiti.writeOutputFile(plan, configuration); wakamiti.generateReports(configuration, snapshot); } /** * Overrides the execution of setup methods annotated with {@code @BeforeClass} for WakamitiJUnitRunner. * *

This method intercepts the execution of setup methods annotated with {@code @BeforeClass} for the * WakamitiJUnitRunner. It prepares the initialization of Wakamiti before executing these setup methods. * If the method to initialize Wakamiti is not found, it throws a WakamitiException indicating the failure.

* * @param statement The statement to be executed, which includes the setup methods annotated with {@code @BeforeClass}. * @return The statement with the intercepted execution of Wakamiti initialization. * @throws WakamitiException If the method to initialize Wakamiti is not found. */ @Override protected Statement withBeforeClasses(Statement statement) { List befores = getTestClass().getAnnotatedMethods(BeforeClass.class); try { Method initWakamiti = this.getClass().getDeclaredMethod("initWakamiti"); initWakamiti.setAccessible(true); statement = new RunBefores(statement, List.of(new FrameworkMethod(initWakamiti)), this); } catch (NoSuchMethodException e) { throw new WakamitiException("Cannot initialize wakamiti runner", e); } return (befores.isEmpty() ? statement : new RunBefores(statement, befores, null)); } /** * Overrides the execution of teardown methods annotated with {@code @AfterClass} for WakamitiJUnitRunner. * *

This method intercepts the execution of teardown methods annotated with {@code @AfterClass} for the * WakamitiJUnitRunner. It prepares the finalization of Wakamiti after executing these teardown methods. * If the method to finalize Wakamiti is not found, it throws a WakamitiException indicating the failure.

* * @param statement The statement to be executed, which includes the teardown methods annotated with {@code @AfterClass}. * @return The statement with the intercepted execution of Wakamiti finalization. * @throws WakamitiException If the method to finalize Wakamiti is not found. */ @Override protected Statement withAfterClasses(Statement statement) { List afters = getTestClass().getAnnotatedMethods(AfterClass.class); try { Method finalizeWakamiti = this.getClass().getDeclaredMethod("finalizeWakamiti"); finalizeWakamiti.setAccessible(true); statement = new RunAfters(statement, List.of(new FrameworkMethod(finalizeWakamiti)), this); } catch (NoSuchMethodException e) { throw new WakamitiException("Cannot finalize wakamiti runner", e); } return (afters.isEmpty() ? statement : new RunAfters(statement, afters, null)); } /** * Validates that no annotated methods are present in the test class. * * @param configurationClass The test class. * @param annotation The annotation to check for. * @param errors The list to collect errors. */ private void validateNoAnnotatedMethod( Class configurationClass, Class annotation, List errors ) { for (Method method : configurationClass.getMethods()) { if (method.isAnnotationPresent(annotation)) { String message = String.format("Method %s annotated with %s is not allowed", method.getName(), annotation.getName()); errors.add(new InitializationError(message)); } } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy