diff --git a/bb-core/src/main/java/com/cognifide/qa/bb/RunWithJunit5.java b/bb-core/src/main/java/com/cognifide/qa/bb/RunWithJunit5.java new file mode 100644 index 00000000..d2a23d18 --- /dev/null +++ b/bb-core/src/main/java/com/cognifide/qa/bb/RunWithJunit5.java @@ -0,0 +1,34 @@ +/*- + * #%L + * Bobcat + * %% + * Copyright (C) 2018 Cognifide Ltd. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.cognifide.qa.bb; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface RunWithJunit5 { + + +} diff --git a/bb-core/src/main/java/com/cognifide/qa/bb/loadable/mapper/TestObjectTypeListener.java b/bb-core/src/main/java/com/cognifide/qa/bb/loadable/mapper/TestObjectTypeListener.java index 900c1fe1..781f4a27 100644 --- a/bb-core/src/main/java/com/cognifide/qa/bb/loadable/mapper/TestObjectTypeListener.java +++ b/bb-core/src/main/java/com/cognifide/qa/bb/loadable/mapper/TestObjectTypeListener.java @@ -13,6 +13,7 @@ */ package com.cognifide.qa.bb.loadable.mapper; +import com.cognifide.qa.bb.RunWithJunit5; import com.google.inject.TypeLiteral; import com.google.inject.spi.TypeEncounter; import com.google.inject.spi.TypeListener; @@ -40,7 +41,7 @@ public void hear(TypeLiteral type, TypeEncounter encounter) { private boolean isApplicable(Class rawType) { boolean result; - if (rawType.isAnnotationPresent(RunWith.class) + if ( (rawType.isAnnotationPresent(RunWith.class) || rawType.isAnnotationPresent(RunWithJunit5.class)) && !rawType.isAnnotationPresent(CucumberOptions.class)) { result = true; } else { diff --git a/bb-junit5/pom.xml b/bb-junit5/pom.xml new file mode 100644 index 00000000..8a25c15d --- /dev/null +++ b/bb-junit5/pom.xml @@ -0,0 +1,68 @@ + + + + 4.0.0 + + bobcat + com.cognifide.qa.bb + 1.4.1-SNAPSHOT + + + bb-junit5 + + + + + com.cognifide.qa.bb + bb-core + ${project.version} + + + + org.junit.jupiter + junit-jupiter-api + + + + org.junit.platform + junit-platform-commons + + + + com.google.inject + guice + + + + + + + org.apache.rat + apache-rat-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + diff --git a/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/GuiceExtension.java b/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/GuiceExtension.java new file mode 100644 index 00000000..f3d2003a --- /dev/null +++ b/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/GuiceExtension.java @@ -0,0 +1,198 @@ +/*- + * #%L + * Bobcat + * %% + * Copyright (C) 2018 Cognifide Ltd. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.cognifide.qa.bb.junit5.guice; + +import static java.util.stream.Collectors.toSet; +import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation; + +import com.google.common.collect.Sets; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.Module; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; + +/** + * Extension that will start guice and is responsible for the injections to test instance Based on + * Guice + * Extension + */ +public class GuiceExtension implements TestInstancePostProcessor { + + private static final Namespace NAMESPACE = + Namespace.create("com", "cognifide", "qa", "bb", "junit", "guice"); + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext context) + throws Exception { + + getOrCreateInjector(context).ifPresent(injector -> injector.injectMembers(testInstance)); + + } + + /** + * Create {@link Injector} or get existing one from test context + */ + private static Optional getOrCreateInjector(ExtensionContext context) + throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + + Optional optionalAnnotatedElement = context.getElement(); + if (!optionalAnnotatedElement.isPresent()) { + return Optional.empty(); + } + + AnnotatedElement element = optionalAnnotatedElement.get(); + Store store = context.getStore(NAMESPACE); + + Injector injector = store.get(element, Injector.class); + if (injector == null) { + injector = createInjector(context); + store.put(element, injector); + } + + return Optional.of(injector); + } + + /** + * Creates {@link Injector} from test context + */ + private static Injector createInjector(ExtensionContext context) + throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + Optional parentInjector = getParentInjector(context); + List modules = getNewModules(context); + + return parentInjector + .map(injector -> injector.createChildInjector(modules)) + .orElseGet(() -> Guice.createInjector(modules)); + } + + /** + * Retrieves {@link Injector} from parent test context + */ + private static Optional getParentInjector(ExtensionContext context) + throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + final Optional optionalParent = context.getParent(); + if (optionalParent.isPresent()) { + return getOrCreateInjector(optionalParent.get()); + } + return Optional.empty(); + } + + /** + * Gets all new {@link Module} instances for injections + */ + private static List getNewModules(ExtensionContext context) + throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { + Set> moduleTypes = getNewModuleTypes(context); + List modules = new ArrayList<>(moduleTypes.size()); + for (Class moduleType : moduleTypes) { + Constructor moduleCtor = moduleType.getDeclaredConstructor(); + moduleCtor.setAccessible(true); + + modules.add(moduleCtor.newInstance()); + } + + context.getElement().ifPresent(element -> { + if (element instanceof Class) { + modules.add(new AbstractModule() { + @Override + protected void configure() { + requestStaticInjection((Class) element); + } + }); + } + }); + + return modules; + } + + /** + * Returns all new {@link Module} declared in current context (returns empty set if modules where + * already declared) + */ + private static Set> getNewModuleTypes(ExtensionContext context) { + Optional optionalAnnotatedElement = context.getElement(); + if (!optionalAnnotatedElement.isPresent()) { + return Collections.emptySet(); + } + + Set> moduleTypes = getModuleTypes(optionalAnnotatedElement.get()); + context.getParent() + .map(GuiceExtension::getContextModuleTypes) + .ifPresent(moduleTypes::removeAll); + + return moduleTypes; + } + + private static Set> getContextModuleTypes(ExtensionContext context) { + return getContextModuleTypes(Optional.of(context)); + } + + /** + * Returns module types that are present on the given context or any of its enclosing contexts. + */ + private static Set> getContextModuleTypes( + Optional context) { + + Set> contextModuleTypes = new LinkedHashSet<>(); + while (context.isPresent() && (hasAnnotatedElement(context) || hasParent(context))) { + context + .flatMap(ExtensionContext::getElement) + .map(GuiceExtension::getModuleTypes) + .ifPresent(contextModuleTypes::addAll); + context = context.flatMap(ExtensionContext::getParent); + } + + return contextModuleTypes; + } + + private static boolean hasAnnotatedElement(Optional context) { + return context.flatMap(ExtensionContext::getElement).isPresent(); + } + + private static boolean hasParent(Optional context) { + return context.flatMap(ExtensionContext::getParent).isPresent(); + } + + private static Set> getModuleTypes(AnnotatedElement element) { + + Optional[]> classes = findAnnotation(element, Modules.class) + .map(Modules::value); + + if (classes.isPresent()) { + return Arrays.stream(classes.get()).collect(toSet()); + } + return Sets.newHashSet(); + + } +} diff --git a/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/InjectorUtils.java b/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/InjectorUtils.java new file mode 100644 index 00000000..5af52d1f --- /dev/null +++ b/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/InjectorUtils.java @@ -0,0 +1,55 @@ +/*- + * #%L + * Bobcat + * %% + * Copyright (C) 2018 Cognifide Ltd. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.cognifide.qa.bb.junit5.guice; + + +import com.google.inject.Injector; +import java.lang.reflect.AnnotatedElement; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; + +/** + * Extensions helper class for retriving injector created in {@link GuiceExtension} + */ +public final class InjectorUtils { + + private InjectorUtils() { + //for util class + } + + /** + * Retrieves injector from the context store using Namespace. If it is not present it searches in + * parent context until it is found or the is end of hierachy + * + * @param context - extension context from junit5 extensions + * @param namespace - Namespace for current test invocation + * @return Injector or null if no injector is found + */ + public static Injector retrieveInjectorFromStore(ExtensionContext context, Namespace namespace) { + AnnotatedElement element = context.getElement() + .orElseThrow(() -> new NoSuchElementException("No element present")); + + return context.getStore(namespace).getOrComputeIfAbsent(element, + absent -> retrieveInjectorFromStore( + context.getParent().orElseThrow(() -> new NoSuchElementException("No injector found")), + namespace), Injector.class); + } +} diff --git a/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/Modules.java b/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/Modules.java new file mode 100644 index 00000000..e07ca1d4 --- /dev/null +++ b/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/Modules.java @@ -0,0 +1,47 @@ +/*- + * #%L + * Bobcat + * %% + * Copyright (C) 2018 Cognifide Ltd. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.cognifide.qa.bb.junit5.guice; + +import com.google.inject.Guice; +import com.google.inject.Module; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + *

+ * Specifies the {@link Guice} modules that define the context of the test. + *

+ *
+ * {@literal @}RunWith(TestRunner.class)
+ * {@literal @}Modules(MyModules.class)
+ *  public class MyTest {
+ *   ...
+ *  }
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Modules { + + Class[] value(); + +} diff --git a/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/WebdriverCloseExtension.java b/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/WebdriverCloseExtension.java new file mode 100644 index 00000000..f5fb1baf --- /dev/null +++ b/bb-junit5/src/main/java/com/cognifide/qa/bb/junit5/guice/WebdriverCloseExtension.java @@ -0,0 +1,46 @@ +/*- + * #%L + * Bobcat + * %% + * Copyright (C) 2018 Cognifide Ltd. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package com.cognifide.qa.bb.junit5.guice; + +import com.google.inject.Injector; +import com.google.inject.Key; +import org.junit.jupiter.api.extension.AfterTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.openqa.selenium.WebDriver; + +/** + * Estension that will close webdriver after the test is executed + */ +public class WebdriverCloseExtension implements AfterTestExecutionCallback { + + + private static final Namespace NAMESPACE = + Namespace.create("com", "cognifide", "qa", "bb", "junit", "guice"); + + @Override + public void afterTestExecution(ExtensionContext context) throws Exception { + Injector injector = InjectorUtils.retrieveInjectorFromStore(context, NAMESPACE); + if (injector != null) { + injector.getInstance(Key.get(WebDriver.class)).quit(); + } + } + +} diff --git a/bb-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/bb-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 00000000..269cc807 --- /dev/null +++ b/bb-junit5/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1,2 @@ +com.cognifide.qa.bb.junit5.guice.GuiceExtension +com.cognifide.qa.bb.junit5.guice.WebdriverCloseExtension \ No newline at end of file diff --git a/pom.xml b/pom.xml index b34ee6aa..1f305085 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,7 @@ limitations under the License. bb-aem-classic bb-aem-touch-ui bb-annotations + bb-junit5 @@ -95,6 +96,19 @@ limitations under the License. + + + org.junit.jupiter + junit-jupiter-api + 5.2.0 + + + + org.junit.platform + junit-platform-commons + 1.2.0 + + org.seleniumhq.selenium selenium-java @@ -314,6 +328,7 @@ limitations under the License. **/*.yaml **/*.properties **/*.Processor + src/main/resources/META-INF/services/**