diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e681bf70e..f93d806768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# v0.8.0 + +## Enhanced spring support + +* when used together with the spring framework JGiven can use a specialized executor so that JGiven stages can be managed via spring +* Introduced `@JGivenStage` to ease writing spring beans that act as JGiven stage + # v0.7.4 ## Fixed Issues diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/Scenario.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/Scenario.java index e3b1fb570f..44189437a5 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/Scenario.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/Scenario.java @@ -10,21 +10,23 @@ */ public class Scenario extends ScenarioBase { - private final GIVEN givenStage; - private final WHEN whenStage; - private final THEN thenStage; + private GIVEN givenStage; + private WHEN whenStage; + private THEN thenStage; + private final Class givenClass; + private final Class whenClass; + private final Class thenClass; - @SuppressWarnings( "unchecked" ) - private Scenario( Class stageClass ) { - givenStage = (GIVEN) executor.addStage( stageClass ); - whenStage = (WHEN) givenStage; - thenStage = (THEN) givenStage; + private Scenario( Class stageClass ) { + this.givenClass = stageClass; + this.whenClass = null; + this.thenClass = null; } public Scenario( Class givenClass, Class whenClass, Class thenClass ) { - givenStage = executor.addStage( givenClass ); - whenStage = executor.addStage( whenClass ); - thenStage = executor.addStage( thenClass ); + this.givenClass = givenClass; + this.whenClass = whenClass; + this.thenClass = thenClass; } public GIVEN getGivenStage() { @@ -84,6 +86,21 @@ public Scenario startScenario( String description ) { } + @Override + @SuppressWarnings("unchecked") + protected void initialize() { + super.initialize(); + if (whenClass == null) { + givenStage = (GIVEN) executor.addStage( givenClass ); + whenStage = (WHEN) givenStage; + thenStage = (THEN) givenStage; + } else { + givenStage = executor.addStage( givenClass ); + whenStage = executor.addStage( whenClass ); + thenStage = executor.addStage( thenClass ); + } + } + /** * Alias for {@link #startScenario(String)}. */ diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java index d4f6cced24..e3f7d4ae9c 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioBase.java @@ -1,18 +1,34 @@ package com.tngtech.jgiven.impl; +import java.lang.reflect.Method; +import java.util.List; + +import com.tngtech.jgiven.impl.util.AssertionUtil; import com.tngtech.jgiven.integration.CanWire; +import com.tngtech.jgiven.report.model.NamedArgument; import com.tngtech.jgiven.report.model.ReportModel; import com.tngtech.jgiven.report.model.ReportModelBuilder; +/** + * Base class for a Scenario. + *

+ * Before a Scenario can be used it must be properly configured. After the configuration phase + * {@link #startScenario} must be called in order to execute the scenario. Once started a scenario + * cannot be reconfigured. + *

+ * {@link #initialize} should be overridden by subclasses to apply their own configuration to the scenario. + * + */ public class ScenarioBase { - protected final ScenarioExecutor executor = new ScenarioExecutor(); + protected ScenarioExecutor executor = new StandaloneScenarioExecutor(); protected final ReportModelBuilder modelBuilder = new ReportModelBuilder(); + private boolean initialized = false; public ScenarioBase() { - executor.setListener( modelBuilder ); } public void setModel( ReportModel scenarioCollectionModel ) { + assertNotInitialized(); modelBuilder.setReportModel( scenarioCollectionModel ); } @@ -26,7 +42,7 @@ public T addStage( Class stepsClass ) { /** * Finishes the scenario. - * + * * @throws Throwable in case some exception has been thrown during the execution of the scenario */ public void finished() throws Throwable { @@ -37,6 +53,11 @@ public ScenarioExecutor getExecutor() { return executor; } + public void setExecutor(ScenarioExecutor executor) { + assertNotInitialized(); + this.executor = executor; + } + public void wireSteps( CanWire canWire ) { executor.wireSteps( canWire ); } @@ -45,9 +66,35 @@ public ReportModelBuilder getModelBuilder() { return modelBuilder; } + public ScenarioBase startScenario(Method method, List arguments) { + performInitialization(); + executor.startScenario(method, arguments); + return this; + } + public ScenarioBase startScenario( String description ) { + performInitialization(); executor.startScenario( description ); return this; } + private void performInitialization() { + if (modelBuilder == null) { + throw new IllegalStateException("modelBuilder must be set before Scenario can be initalized."); + } + if (!initialized) { + executor.setListener( modelBuilder ); + initialize(); + initialized = true; + } + } + + protected void initialize() { + // extension point for two phase initialization + } + + protected void assertNotInitialized() { + AssertionUtil.assertTrue(!initialized, "Scenario is already initialized"); + } + } diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java index db3d3c4977..286e85de8c 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/ScenarioExecutor.java @@ -1,45 +1,13 @@ package com.tngtech.jgiven.impl; -import static com.google.common.collect.Lists.newArrayList; -import static com.google.common.collect.Lists.reverse; -import static com.tngtech.jgiven.impl.ScenarioExecutor.State.FINISHED; -import static com.tngtech.jgiven.impl.ScenarioExecutor.State.STARTED; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.google.common.base.Optional; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.tngtech.jgiven.CurrentStep; -import com.tngtech.jgiven.annotation.*; -import com.tngtech.jgiven.attachment.Attachment; -import com.tngtech.jgiven.exception.FailIfPassedException; -import com.tngtech.jgiven.exception.JGivenUserException; -import com.tngtech.jgiven.impl.inject.ValueInjector; -import com.tngtech.jgiven.impl.intercept.*; -import com.tngtech.jgiven.impl.util.FieldCache; -import com.tngtech.jgiven.impl.util.ParameterNameUtil; -import com.tngtech.jgiven.impl.util.ReflectionUtil; -import com.tngtech.jgiven.impl.util.ReflectionUtil.MethodAction; +import com.tngtech.jgiven.impl.intercept.ScenarioListener; import com.tngtech.jgiven.integration.CanWire; import com.tngtech.jgiven.report.model.NamedArgument; -import net.sf.cglib.proxy.Enhancer; - -/** - * Main class of JGiven for executing scenarios. - */ -public class ScenarioExecutor { - private static final Logger log = LoggerFactory.getLogger( ScenarioExecutor.class ); +public interface ScenarioExecutor { public enum State { INIT, @@ -47,378 +15,40 @@ public enum State { FINISHED } - private Object currentStage; - private State state = State.INIT; - private boolean beforeStepsWereExecuted; - - /** - * Whether life cycle methods should be executed. - * This is only false for scenarios that are annotated with @NotImplementedYet - */ - private boolean executeLifeCycleMethods = true; - - /** - * Measures the stack depth of methods called on the step definition object. - * Only the top-level method calls are used for reporting. - */ - private final AtomicInteger stackDepth = new AtomicInteger(); - - private final Map, StageState> stages = Maps.newLinkedHashMap(); - - private final List scenarioRules = Lists.newArrayList(); - - private final ValueInjector injector = new ValueInjector(); - private ScenarioListener listener = new NoOpScenarioListener(); - private final StepMethodHandler methodHandler = new MethodHandler(); - private final StepMethodInterceptor methodInterceptor = new StepMethodInterceptor( methodHandler, stackDepth ); - private Throwable failedException; - private boolean failIfPass; - private boolean suppressExceptions; - - public ScenarioExecutor() { - injector.injectValueByType( ScenarioExecutor.class, this ); - injector.injectValueByType( CurrentStep.class, new StepAccessImpl() ); - } - - static class StageState { - final Object instance; - boolean afterStageCalled; - boolean beforeStageCalled; - - StageState( Object instance ) { - this.instance = instance; - } - } - - class StepAccessImpl implements CurrentStep { - - @Override - public void addAttachment( Attachment attachment ) { - listener.attachmentAdded( attachment ); - } - - @Override - public void setExtendedDescription( String extendedDescription ) { - listener.extendedDescriptionUpdated( extendedDescription ); - } - } - - class MethodHandler implements StepMethodHandler { - @Override - public void handleMethod( Object stageInstance, Method paramMethod, Object[] arguments, InvocationMode mode ) - throws Throwable { - - if( paramMethod.isSynthetic() && !paramMethod.isBridge() ) { - return; - } - - if( paramMethod.isAnnotationPresent( AfterStage.class ) - || paramMethod.isAnnotationPresent( BeforeStage.class ) - || paramMethod.isAnnotationPresent( BeforeScenario.class ) - || paramMethod.isAnnotationPresent( AfterScenario.class ) ) { - return; - } + T addStage(Class stepsClass); - update( stageInstance ); + T createStageClass(Class stepsClass); - if( paramMethod.isAnnotationPresent( Hidden.class ) ) { - return; - } + void addIntroWord(String word); - List namedArguments = ParameterNameUtil.mapArgumentsWithParameterNames( paramMethod, - Arrays.asList( arguments ) ); - listener.stepMethodInvoked( paramMethod, namedArguments, mode ); - } - - @Override - public void handleThrowable( Throwable t ) throws Throwable { - listener.stepMethodFailed( t ); - failed( t ); - } - - @Override - public void handleMethodFinished( long durationInNanos ) { - listener.stepMethodFinished( durationInNanos ); - } - - } - - @SuppressWarnings( "unchecked" ) - public T addStage( Class stepsClass ) { - if( stages.containsKey( stepsClass ) ) { - return (T) stages.get( stepsClass ).instance; - } - - T result = setupCglibProxy( stepsClass ); - - stages.put( stepsClass, new StageState( result ) ); - gatherRules( result ); - injectSteps( result ); - return result; - } - - @SuppressWarnings( "unchecked" ) - private T setupCglibProxy( Class stepsClass ) { - Enhancer e = new Enhancer(); - e.setSuperclass( stepsClass ); - e.setCallback( methodInterceptor ); - T result = (T) e.create(); - methodInterceptor.enableMethodHandling( true ); - return result; - } - - public void addIntroWord( String word ) { - listener.introWordAdded( word ); - } - - @SuppressWarnings( "unchecked" ) - private void gatherRules( Object stage ) { - for( Field field : FieldCache.get( stage.getClass() ).getFieldsWithAnnotation( ScenarioRule.class ) ) { - log.debug( "Found rule in field {} ", field ); - try { - scenarioRules.add( field.get( stage ) ); - } catch( IllegalAccessException e ) { - throw new RuntimeException( "Error while reading field " + field, e ); - } - } - - } - - private T update( T t ) throws Throwable { - if( currentStage == t ) { // NOSONAR: reference comparison OK here - return t; - } - - if( currentStage == null ) { - ensureBeforeStepsAreExecuted(); - } else { - executeAfterStageMethods( currentStage ); - readScenarioState( currentStage ); - } - - injector.updateValues( t ); - - StageState stageState = getStageState( t ); - if( !stageState.beforeStageCalled ) { - stageState.beforeStageCalled = true; - executeBeforeStageSteps( t ); - } - - currentStage = t; - return t; - } - - private void executeAfterStageMethods( Object stage ) throws Throwable { - StageState stageState = getStageState( stage ); - if( stageState.afterStageCalled ) { - return; - } - stageState.afterStageCalled = true; - executeAnnotatedMethods( stage, AfterStage.class ); - } - - StageState getStageState( Object stage ) { - return stages.get( stage.getClass().getSuperclass() ); - } - - private void ensureBeforeStepsAreExecuted() throws Throwable { - if( state != State.INIT ) { - return; - } - state = State.STARTED; - methodInterceptor.enableMethodHandling( false ); - - try { - for( Object rule : scenarioRules ) { - invokeRuleMethod( rule, "before" ); - } - - beforeStepsWereExecuted = true; - - for( StageState stage : stages.values() ) { - executeBeforeScenarioSteps( stage.instance ); - } - } catch( Throwable e ) { - failed( e ); - finished(); - throw e; - } - - methodInterceptor.enableMethodHandling( true ); - } - - private void executeAnnotatedMethods( Object stage, final Class annotationClass ) throws Throwable { - if( !executeLifeCycleMethods ) { - return; - } - - log.debug( "Executing methods annotated with @{}", annotationClass.getName() ); - boolean previousMethodExecution = methodInterceptor.enableMethodExecution( true ); - try { - methodInterceptor.enableMethodHandling( false ); - ReflectionUtil.forEachMethod( stage, stage.getClass(), annotationClass, new MethodAction() { - @Override - public void act( Object object, Method method ) throws Exception { - ReflectionUtil.invokeMethod( object, method, " with annotation @" + annotationClass.getName() ); - } - } ); - methodInterceptor.enableMethodHandling( true ); - } catch( JGivenUserException e ) { - throw e.getCause(); - } finally { - methodInterceptor.enableMethodExecution( previousMethodExecution ); - } - } - - private void invokeRuleMethod( Object rule, String methodName ) throws Throwable { - if( !executeLifeCycleMethods ) { - return; - } - - Optional optionalMethod = ReflectionUtil.findMethodTransitively( rule.getClass(), methodName ); - if( !optionalMethod.isPresent() ) { - log.debug( "Class {} has no {} method, but was used as ScenarioRule!", rule.getClass(), methodName ); - return; - } - - try { - ReflectionUtil.invokeMethod( rule, optionalMethod.get(), " of rule class " + rule.getClass().getName() ); - } catch( JGivenUserException e ) { - throw e.getCause(); - } - } - - void executeBeforeStageSteps( Object stage ) throws Throwable { - executeAnnotatedMethods( stage, BeforeStage.class ); - } - - private void executeBeforeScenarioSteps( Object stage ) throws Throwable { - executeAnnotatedMethods( stage, BeforeScenario.class ); - } - - public void readScenarioState( Object object ) { - injector.readValues( object ); - } + void readScenarioState(Object object); /** * Used for DI frameworks to inject values into stages. */ - public void wireSteps( CanWire canWire ) { - for( StageState steps : stages.values() ) { - canWire.wire( steps.instance ); - } - } + void wireSteps(CanWire canWire); /** * Has to be called when the scenario is finished in order to execute after methods. */ - public void finished() throws Throwable { - if( state == FINISHED ) { - return; - } - - State previousState = state; + void finished() throws Throwable; - state = FINISHED; - methodInterceptor.enableMethodHandling( false ); + void injectSteps(Object stage); - try { - if( previousState == STARTED ) { - callFinishLifeCycleMethods(); - } - } finally { - listener.scenarioFinished(); - } - } - - private void callFinishLifeCycleMethods() throws Throwable { - Throwable firstThrownException = failedException; - if( beforeStepsWereExecuted ) { - if( currentStage != null ) { - try { - executeAfterStageMethods( currentStage ); - } catch( AssertionError e ) { - firstThrownException = logAndGetFirstException( firstThrownException, e ); - } catch( Exception e ) { - firstThrownException = logAndGetFirstException( firstThrownException, e ); - } - } - - for( StageState stage : reverse( newArrayList( stages.values() ) ) ) { - try { - executeAnnotatedMethods( stage.instance, AfterScenario.class ); - } catch( AssertionError e ) { - firstThrownException = logAndGetFirstException( firstThrownException, e ); - } catch( Exception e ) { - firstThrownException = logAndGetFirstException( firstThrownException, e ); - } - } - } - - for( Object rule : Lists.reverse( scenarioRules ) ) { - try { - invokeRuleMethod( rule, "after" ); - } catch( AssertionError e ) { - firstThrownException = logAndGetFirstException( firstThrownException, e ); - } catch( Exception e ) { - firstThrownException = logAndGetFirstException( firstThrownException, e ); - } - } - - failedException = firstThrownException; - - if( !suppressExceptions && failedException != null ) { - throw failedException; - } + boolean hasFailed(); - if( failIfPass && failedException == null ) { - throw new FailIfPassedException(); - } - } - - private Throwable logAndGetFirstException( Throwable firstThrownException, Throwable newException ) { - log.error( newException.getMessage(), newException ); - return firstThrownException == null ? newException : firstThrownException; - } + Throwable getFailedException(); - @SuppressWarnings( "unchecked" ) - public void injectSteps( Object stage ) { - for( Field field : FieldCache.get( stage.getClass() ).getFieldsWithAnnotation( ScenarioStage.class ) ) { - Object steps = addStage( field.getType() ); - ReflectionUtil.setField( field, stage, steps, ", annotated with @ScenarioStage" ); - } - } + void setFailedException(Exception e); - public boolean hasFailed() { - return failedException != null; - } - - public Throwable getFailedException() { - return failedException; - } - - public void setFailedException( Exception e ) { - failedException = e; - } - - public void failed( Throwable e ) { - if( hasFailed() ) { - log.error( e.getMessage(), e ); - } else { - listener.scenarioFailed( e ); - methodInterceptor.disableMethodExecution(); - failedException = e; - } - } + void failed(Throwable e); /** * Starts a scenario with the given description. * * @param description the description of the scenario */ - public void startScenario( String description ) { - listener.scenarioStarted( description ); - } + void startScenario(String description); /** * Starts the scenario with the given method and arguments. @@ -426,28 +56,10 @@ public void startScenario( String description ) { * @param method the method that started the scenario * @param arguments the test arguments with their parameter names */ - public void startScenario( Method method, List arguments ) { - listener.scenarioStarted( method, arguments ); + void startScenario(Method method, List arguments); - if( method.isAnnotationPresent( NotImplementedYet.class ) ) { - NotImplementedYet annotation = method.getAnnotation( NotImplementedYet.class ); + void setListener(ScenarioListener listener); - if( annotation.failIfPass() ) { - failIfPass(); - } else if( !annotation.executeSteps() ) { - methodInterceptor.disableMethodExecution(); - executeLifeCycleMethods = false; - } - suppressExceptions = true; - } - } - - public void setListener( ScenarioListener listener ) { - this.listener = listener; - } - - public void failIfPass() { - failIfPass = true; - } + void failIfPass(); -} +} \ No newline at end of file diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/StandaloneScenarioExecutor.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/StandaloneScenarioExecutor.java new file mode 100644 index 0000000000..90396f6b47 --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/StandaloneScenarioExecutor.java @@ -0,0 +1,473 @@ +package com.tngtech.jgiven.impl; + +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Lists.reverse; +import static com.tngtech.jgiven.impl.ScenarioExecutor.State.FINISHED; +import static com.tngtech.jgiven.impl.ScenarioExecutor.State.STARTED; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import net.sf.cglib.proxy.Enhancer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Optional; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.tngtech.jgiven.CurrentStep; +import com.tngtech.jgiven.annotation.AfterScenario; +import com.tngtech.jgiven.annotation.AfterStage; +import com.tngtech.jgiven.annotation.BeforeScenario; +import com.tngtech.jgiven.annotation.BeforeStage; +import com.tngtech.jgiven.annotation.Hidden; +import com.tngtech.jgiven.annotation.NotImplementedYet; +import com.tngtech.jgiven.annotation.ScenarioRule; +import com.tngtech.jgiven.annotation.ScenarioStage; +import com.tngtech.jgiven.attachment.Attachment; +import com.tngtech.jgiven.exception.FailIfPassedException; +import com.tngtech.jgiven.exception.JGivenUserException; +import com.tngtech.jgiven.impl.inject.ValueInjector; +import com.tngtech.jgiven.impl.intercept.InvocationMode; +import com.tngtech.jgiven.impl.intercept.NoOpScenarioListener; +import com.tngtech.jgiven.impl.intercept.ScenarioListener; +import com.tngtech.jgiven.impl.intercept.StandaloneStepMethodInterceptor; +import com.tngtech.jgiven.impl.intercept.StepMethodHandler; +import com.tngtech.jgiven.impl.util.FieldCache; +import com.tngtech.jgiven.impl.util.ParameterNameUtil; +import com.tngtech.jgiven.impl.util.ReflectionUtil; +import com.tngtech.jgiven.impl.util.ReflectionUtil.MethodAction; +import com.tngtech.jgiven.integration.CanWire; +import com.tngtech.jgiven.report.model.NamedArgument; + +/** + * Main class of JGiven for executing scenarios. + */ +public class StandaloneScenarioExecutor implements ScenarioExecutor { + private static final Logger log = LoggerFactory.getLogger( StandaloneScenarioExecutor.class ); + + private Object currentStage; + private State state = State.INIT; + private boolean beforeStepsWereExecuted; + + /** + * Whether life cycle methods should be executed. + * This is only false for scenarios that are annotated with @NotImplementedYet + */ + private boolean executeLifeCycleMethods = true; + + /** + * Measures the stack depth of methods called on the step definition object. + * Only the top-level method calls are used for reporting. + */ + protected final AtomicInteger stackDepth = new AtomicInteger(); + + protected final Map, StageState> stages = Maps.newLinkedHashMap(); + + private final List scenarioRules = Lists.newArrayList(); + + private final ValueInjector injector = new ValueInjector(); + private ScenarioListener listener = new NoOpScenarioListener(); + protected final StepMethodHandler methodHandler = new MethodHandler(); + private final StandaloneStepMethodInterceptor methodInterceptor = new StandaloneStepMethodInterceptor( methodHandler, stackDepth ); + private Throwable failedException; + private boolean failIfPass; + private boolean suppressExceptions; + + public StandaloneScenarioExecutor() { + injector.injectValueByType( StandaloneScenarioExecutor.class, this ); + injector.injectValueByType( CurrentStep.class, new StepAccessImpl() ); + } + + protected static class StageState { + final Object instance; + boolean afterStageCalled; + boolean beforeStageCalled; + + StageState( Object instance ) { + this.instance = instance; + } + } + + class StepAccessImpl implements CurrentStep { + + @Override + public void addAttachment( Attachment attachment ) { + listener.attachmentAdded( attachment ); + } + + @Override + public void setExtendedDescription( String extendedDescription ) { + listener.extendedDescriptionUpdated( extendedDescription ); + } + } + + class MethodHandler implements StepMethodHandler { + @Override + public void handleMethod( Object stageInstance, Method paramMethod, Object[] arguments, InvocationMode mode ) + throws Throwable { + + if( paramMethod.isSynthetic() && !paramMethod.isBridge() ) { + return; + } + + if( paramMethod.isAnnotationPresent( AfterStage.class ) + || paramMethod.isAnnotationPresent( BeforeStage.class ) + || paramMethod.isAnnotationPresent( BeforeScenario.class ) + || paramMethod.isAnnotationPresent( AfterScenario.class ) ) { + return; + } + + update( stageInstance ); + + if( paramMethod.isAnnotationPresent( Hidden.class ) ) { + return; + } + + List namedArguments = ParameterNameUtil.mapArgumentsWithParameterNames( paramMethod, + Arrays.asList( arguments ) ); + listener.stepMethodInvoked( paramMethod, namedArguments, mode ); + } + + @Override + public void handleThrowable( Throwable t ) throws Throwable { + listener.stepMethodFailed( t ); + failed( t ); + } + + @Override + public void handleMethodFinished( long durationInNanos ) { + listener.stepMethodFinished( durationInNanos ); + } + + } + + @Override + @SuppressWarnings( "unchecked" ) + public T addStage( Class stepsClass ) { + if( stages.containsKey( stepsClass ) ) { + return (T) stages.get( stepsClass ).instance; + } + + T result = createStageClass( stepsClass ); + + stages.put( stepsClass, new StageState( result ) ); + gatherRules( result ); + injectSteps( result ); + return result; + } + + @SuppressWarnings( "unchecked" ) + @Override + public T createStageClass( Class stepsClass ) { + Enhancer e = new Enhancer(); + e.setSuperclass( stepsClass ); + e.setCallback( methodInterceptor ); + T result = (T) e.create(); + methodInterceptor.enableMethodHandling( true ); + return result; + } + + @Override + public void addIntroWord( String word ) { + listener.introWordAdded( word ); + } + + @SuppressWarnings( "unchecked" ) + private void gatherRules( Object stage ) { + for( Field field : FieldCache.get( stage.getClass() ).getFieldsWithAnnotation( ScenarioRule.class ) ) { + log.debug( "Found rule in field {} ", field ); + try { + scenarioRules.add( field.get( stage ) ); + } catch( IllegalAccessException e ) { + throw new RuntimeException( "Error while reading field " + field, e ); + } + } + + } + + private T update( T t ) throws Throwable { + if( currentStage == t ) { // NOSONAR: reference comparison OK here + return t; + } + + if( currentStage == null ) { + ensureBeforeStepsAreExecuted(); + } else { + executeAfterStageMethods( currentStage ); + readScenarioState( currentStage ); + } + + injector.updateValues( t ); + + StageState stageState = getStageState( t ); + if( !stageState.beforeStageCalled ) { + stageState.beforeStageCalled = true; + executeBeforeStageSteps( t ); + } + + currentStage = t; + return t; + } + + private void executeAfterStageMethods( Object stage ) throws Throwable { + StageState stageState = getStageState( stage ); + if( stageState.afterStageCalled ) { + return; + } + stageState.afterStageCalled = true; + executeAnnotatedMethods( stage, AfterStage.class ); + } + + public StageState getStageState( Object stage ) { + return stages.get( stage.getClass().getSuperclass() ); + } + + private void ensureBeforeStepsAreExecuted() throws Throwable { + if( state != State.INIT ) { + return; + } + state = State.STARTED; + methodInterceptor.enableMethodHandling( false ); + + try { + for( Object rule : scenarioRules ) { + invokeRuleMethod( rule, "before" ); + } + + beforeStepsWereExecuted = true; + + for( StageState stage : stages.values() ) { + executeBeforeScenarioSteps( stage.instance ); + } + } catch( Throwable e ) { + failed( e ); + finished(); + throw e; + } + + methodInterceptor.enableMethodHandling( true ); + } + + private void executeAnnotatedMethods( Object stage, final Class annotationClass ) throws Throwable { + if( !executeLifeCycleMethods ) { + return; + } + + log.debug( "Executing methods annotated with @{}", annotationClass.getName() ); + boolean previousMethodExecution = methodInterceptor.enableMethodExecution( true ); + try { + methodInterceptor.enableMethodHandling( false ); + ReflectionUtil.forEachMethod( stage, stage.getClass(), annotationClass, new MethodAction() { + @Override + public void act( Object object, Method method ) throws Exception { + ReflectionUtil.invokeMethod( object, method, " with annotation @" + annotationClass.getName() ); + } + } ); + methodInterceptor.enableMethodHandling( true ); + } catch( JGivenUserException e ) { + throw e.getCause(); + } finally { + methodInterceptor.enableMethodExecution( previousMethodExecution ); + } + } + + private void invokeRuleMethod( Object rule, String methodName ) throws Throwable { + if( !executeLifeCycleMethods ) { + return; + } + + Optional optionalMethod = ReflectionUtil.findMethodTransitively( rule.getClass(), methodName ); + if( !optionalMethod.isPresent() ) { + log.debug( "Class {} has no {} method, but was used as ScenarioRule!", rule.getClass(), methodName ); + return; + } + + try { + ReflectionUtil.invokeMethod( rule, optionalMethod.get(), " of rule class " + rule.getClass().getName() ); + } catch( JGivenUserException e ) { + throw e.getCause(); + } + } + + void executeBeforeStageSteps( Object stage ) throws Throwable { + executeAnnotatedMethods( stage, BeforeStage.class ); + } + + private void executeBeforeScenarioSteps( Object stage ) throws Throwable { + executeAnnotatedMethods( stage, BeforeScenario.class ); + } + + @Override + public void readScenarioState( Object object ) { + injector.readValues( object ); + } + + /** + * Used for DI frameworks to inject values into stages. + */ + @Override + public void wireSteps( CanWire canWire ) { + for( StageState steps : stages.values() ) { + canWire.wire( steps.instance ); + } + } + + /** + * Has to be called when the scenario is finished in order to execute after methods. + */ + @Override + public void finished() throws Throwable { + if( state == FINISHED ) { + return; + } + + State previousState = state; + + state = FINISHED; + methodInterceptor.enableMethodHandling( false ); + + try { + if( previousState == STARTED ) { + callFinishLifeCycleMethods(); + } + } finally { + listener.scenarioFinished(); + } + } + + private void callFinishLifeCycleMethods() throws Throwable { + Throwable firstThrownException = failedException; + if( beforeStepsWereExecuted ) { + if( currentStage != null ) { + try { + executeAfterStageMethods( currentStage ); + } catch( AssertionError e ) { + firstThrownException = logAndGetFirstException( firstThrownException, e ); + } catch( Exception e ) { + firstThrownException = logAndGetFirstException( firstThrownException, e ); + } + } + + for( StageState stage : reverse( newArrayList( stages.values() ) ) ) { + try { + executeAnnotatedMethods( stage.instance, AfterScenario.class ); + } catch( AssertionError e ) { + firstThrownException = logAndGetFirstException( firstThrownException, e ); + } catch( Exception e ) { + firstThrownException = logAndGetFirstException( firstThrownException, e ); + } + } + } + + for( Object rule : Lists.reverse( scenarioRules ) ) { + try { + invokeRuleMethod( rule, "after" ); + } catch( AssertionError e ) { + firstThrownException = logAndGetFirstException( firstThrownException, e ); + } catch( Exception e ) { + firstThrownException = logAndGetFirstException( firstThrownException, e ); + } + } + + failedException = firstThrownException; + + if( !suppressExceptions && failedException != null ) { + throw failedException; + } + + if( failIfPass && failedException == null ) { + throw new FailIfPassedException(); + } + } + + private Throwable logAndGetFirstException( Throwable firstThrownException, Throwable newException ) { + log.error( newException.getMessage(), newException ); + return firstThrownException == null ? newException : firstThrownException; + } + + @Override + @SuppressWarnings( "unchecked" ) + public void injectSteps( Object stage ) { + for( Field field : FieldCache.get( stage.getClass() ).getFieldsWithAnnotation( ScenarioStage.class ) ) { + Object steps = addStage( field.getType() ); + ReflectionUtil.setField( field, stage, steps, ", annotated with @ScenarioStage" ); + } + } + + @Override + public boolean hasFailed() { + return failedException != null; + } + + @Override + public Throwable getFailedException() { + return failedException; + } + + @Override + public void setFailedException( Exception e ) { + failedException = e; + } + + @Override + public void failed( Throwable e ) { + if( hasFailed() ) { + log.error( e.getMessage(), e ); + } else { + listener.scenarioFailed( e ); + methodInterceptor.disableMethodExecution(); + failedException = e; + } + } + + /** + * Starts a scenario with the given description. + * + * @param description the description of the scenario + */ + @Override + public void startScenario( String description ) { + listener.scenarioStarted( description ); + } + + /** + * Starts the scenario with the given method and arguments. + * Derives the description from the method name. + * @param method the method that started the scenario + * @param arguments the test arguments with their parameter names + */ + @Override + public void startScenario( Method method, List arguments ) { + listener.scenarioStarted( method, arguments ); + + if( method.isAnnotationPresent( NotImplementedYet.class ) ) { + NotImplementedYet annotation = method.getAnnotation( NotImplementedYet.class ); + + if( annotation.failIfPass() ) { + failIfPass(); + } else if( !annotation.executeSteps() ) { + methodInterceptor.disableMethodExecution(); + executeLifeCycleMethods = false; + } + suppressExceptions = true; + } + } + + @Override + public void setListener( ScenarioListener listener ) { + this.listener = listener; + } + + @Override + public void failIfPass() { + failIfPass = true; + } + +} diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StandaloneStepMethodInterceptor.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StandaloneStepMethodInterceptor.java new file mode 100644 index 0000000000..7e42805170 --- /dev/null +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StandaloneStepMethodInterceptor.java @@ -0,0 +1,35 @@ +package com.tngtech.jgiven.impl.intercept; + +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicInteger; + +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +/** + * StepMethodInterceptor that uses cglib {@link MethodInterceptor} for intercepting JGiven methods + * + */ +public class StandaloneStepMethodInterceptor extends StepMethodInterceptor implements MethodInterceptor { + + public StandaloneStepMethodInterceptor( + StepMethodHandler scenarioMethodHandler, AtomicInteger stackDepth) { + super(scenarioMethodHandler, stackDepth); + } + + @Override + public Object intercept( final Object receiver, Method method, final Object[] parameters, final MethodProxy methodProxy ) throws Throwable { + Invoker invoker = new Invoker() { + + @Override + public Object proceed() throws Throwable { + return methodProxy.invokeSuper( receiver, parameters ); + } + + }; + return doIntercept(receiver, method, parameters, invoker); + } + + + +} diff --git a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodInterceptor.java b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodInterceptor.java index a109aa1210..e1580593f0 100644 --- a/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodInterceptor.java +++ b/jgiven-core/src/main/java/com/tngtech/jgiven/impl/intercept/StepMethodInterceptor.java @@ -1,6 +1,8 @@ package com.tngtech.jgiven.impl.intercept; -import static com.tngtech.jgiven.impl.intercept.InvocationMode.*; +import static com.tngtech.jgiven.impl.intercept.InvocationMode.NORMAL; +import static com.tngtech.jgiven.impl.intercept.InvocationMode.NOT_IMPLEMENTED_YET; +import static com.tngtech.jgiven.impl.intercept.InvocationMode.SKIPPED; import java.lang.reflect.Method; import java.util.concurrent.atomic.AtomicInteger; @@ -10,14 +12,12 @@ import com.tngtech.jgiven.annotation.NotImplementedYet; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; - -public class StepMethodInterceptor implements MethodInterceptor { +public class StepMethodInterceptor { private static final Logger log = LoggerFactory.getLogger( StepMethodInterceptor.class ); - private final StepMethodHandler scenarioMethodHandler; - private final AtomicInteger stackDepth; + private StepMethodHandler scenarioMethodHandler; + + private AtomicInteger stackDepth; /** * Whether the method handler is called when a step method is invoked @@ -29,13 +29,21 @@ public class StepMethodInterceptor implements MethodInterceptor { */ private boolean methodExecutionEnabled = true; + /** + * abstraction to continue intercepted method + */ + public interface Invoker { + Object proceed() throws Throwable; + }; + public StepMethodInterceptor( StepMethodHandler scenarioMethodHandler, AtomicInteger stackDepth ) { this.scenarioMethodHandler = scenarioMethodHandler; this.stackDepth = stackDepth; } - @Override - public Object intercept( Object receiver, Method method, Object[] parameters, MethodProxy methodProxy ) throws Throwable { + + public final Object doIntercept(final Object receiver, Method method, + final Object[] parameters, Invoker invoker) throws Throwable { long started = System.nanoTime(); InvocationMode mode = getInvocationMode( receiver, method ); @@ -50,9 +58,9 @@ public Object intercept( Object receiver, Method method, Object[] parameters, Me try { stackDepth.incrementAndGet(); - return methodProxy.invokeSuper( receiver, parameters ); - } catch( Exception t ) { - return handleThrowable( receiver, method, t, System.nanoTime() - started ); + return invoker.proceed(); + } catch (Exception e) { + return handleThrowable( receiver, method, e, System.nanoTime() - started ); } catch( AssertionError e ) { return handleThrowable( receiver, method, e, System.nanoTime() - started ); } finally { @@ -63,7 +71,7 @@ public Object intercept( Object receiver, Method method, Object[] parameters, Me } } - private Object handleThrowable( Object receiver, Method method, Throwable t, long durationInNanos ) throws Throwable { + protected Object handleThrowable( Object receiver, Method method, Throwable t, long durationInNanos ) throws Throwable { if( methodHandlingEnabled ) { scenarioMethodHandler.handleThrowable( t ); return returnReceiverOrNull( receiver, method ); @@ -71,7 +79,7 @@ private Object handleThrowable( Object receiver, Method method, Throwable t, lon throw t; } - private Object returnReceiverOrNull( Object receiver, Method method ) { + protected Object returnReceiverOrNull( Object receiver, Method method ) { // we assume here that the implementation follows the fluent interface // convention and returns the receiver object. If not, we fall back to null // and hope for the best. @@ -88,7 +96,7 @@ private Object returnReceiverOrNull( Object receiver, Method method ) { return receiver; } - private InvocationMode getInvocationMode( Object receiver, Method method ) { + protected InvocationMode getInvocationMode( Object receiver, Method method ) { if( method.getDeclaringClass() == Object.class ) { return NORMAL; } @@ -120,4 +128,24 @@ public boolean enableMethodExecution( boolean b ) { return previousMethodExecution; } + public StepMethodHandler getScenarioMethodHandler() { + return scenarioMethodHandler; + } + + + public AtomicInteger getStackDepth() { + return stackDepth; + } + + + public void setScenarioMethodHandler(StepMethodHandler scenarioMethodHandler) { + this.scenarioMethodHandler = scenarioMethodHandler; + } + + + public void setStackDepth(AtomicInteger stackDepth) { + this.stackDepth = stackDepth; + } + + } diff --git a/jgiven-core/src/test/java/com/tngtech/jgiven/impl/ScenarioExecutorTest.java b/jgiven-core/src/test/java/com/tngtech/jgiven/impl/StandaloneScenarioExecutorTest.java similarity index 84% rename from jgiven-core/src/test/java/com/tngtech/jgiven/impl/ScenarioExecutorTest.java rename to jgiven-core/src/test/java/com/tngtech/jgiven/impl/StandaloneScenarioExecutorTest.java index 6e0059d1dc..570273501a 100644 --- a/jgiven-core/src/test/java/com/tngtech/jgiven/impl/ScenarioExecutorTest.java +++ b/jgiven-core/src/test/java/com/tngtech/jgiven/impl/StandaloneScenarioExecutorTest.java @@ -2,27 +2,25 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.lang.reflect.Method; -import java.util.List; - import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import com.google.common.collect.Lists; -import com.tngtech.jgiven.annotation.*; +import com.tngtech.jgiven.annotation.AfterStage; +import com.tngtech.jgiven.annotation.BeforeStage; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.annotation.NotImplementedYet; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import com.tngtech.jgiven.annotation.ScenarioStage; import com.tngtech.jgiven.exception.JGivenExecutionException; -import com.tngtech.jgiven.impl.intercept.InvocationMode; -import com.tngtech.jgiven.impl.intercept.NoOpScenarioListener; -import com.tngtech.jgiven.report.model.NamedArgument; -public class ScenarioExecutorTest { +public class StandaloneScenarioExecutorTest { @Rule public final ExpectedException expectedExceptionRule = ExpectedException.none(); @Test public void methods_annotated_with_BeforeStage_are_executed_before_the_first_step_is_executed() { - ScenarioExecutor executor = new ScenarioExecutor(); + ScenarioExecutor executor = new StandaloneScenarioExecutor(); BeforeStageStep steps = executor.addStage( BeforeStageStep.class ); executor.startScenario( "Test" ); steps.before_stage_was_executed(); @@ -30,7 +28,7 @@ public void methods_annotated_with_BeforeStage_are_executed_before_the_first_ste @Test public void methods_annotated_with_AfterStage_are_executed_before_the_first_step_of_the_next_stage_is_executed() { - ScenarioExecutor executor = new ScenarioExecutor(); + ScenarioExecutor executor = new StandaloneScenarioExecutor(); AfterStageStep steps = executor.addStage( AfterStageStep.class ); NextSteps nextSteps = executor.addStage( NextSteps.class ); executor.startScenario( "Test" ); @@ -40,7 +38,7 @@ public void methods_annotated_with_AfterStage_are_executed_before_the_first_step @Test public void methods_annotated_with_NotImplementedYet_are_not_really_executed() { - ScenarioExecutor executor = new ScenarioExecutor(); + ScenarioExecutor executor = new StandaloneScenarioExecutor(); NotImplementedYetTestStep steps = executor.addStage( NotImplementedYetTestStep.class ); executor.startScenario( "Test" ); steps.something_not_implemented_yet(); @@ -49,7 +47,7 @@ public void methods_annotated_with_NotImplementedYet_are_not_really_executed() { @Test public void methods_annotated_with_NotImplemented_must_follow_fluent_interface_convention_or_return_null() { - ScenarioExecutor executor = new ScenarioExecutor(); + ScenarioExecutor executor = new StandaloneScenarioExecutor(); NotImplementedYetTestStep steps = executor.addStage( NotImplementedYetTestStep.class ); executor.startScenario( "Test" ); assertThat( steps.something_not_implemented_yet_with_wrong_signature() ).isNull(); @@ -57,7 +55,7 @@ public void methods_annotated_with_NotImplemented_must_follow_fluent_interface_c @Test public void stepclasses_annotated_with_NotImplementedYet_are_not_really_executed() { - ScenarioExecutor executor = new ScenarioExecutor(); + ScenarioExecutor executor = new StandaloneScenarioExecutor(); NotImplementedYetTestStepClass steps = executor.addStage( NotImplementedYetTestStepClass.class ); executor.startScenario( "Test" ); steps.something_not_implemented_yet(); @@ -66,7 +64,7 @@ public void stepclasses_annotated_with_NotImplementedYet_are_not_really_executed @Test public void steps_are_injected() { - ScenarioExecutor executor = new ScenarioExecutor(); + ScenarioExecutor executor = new StandaloneScenarioExecutor(); TestClass testClass = new TestClass(); executor.injectSteps( testClass ); @@ -76,7 +74,7 @@ public void steps_are_injected() { @Test public void recursive_steps_are_injected_correctly() { - ScenarioExecutor executor = new ScenarioExecutor(); + ScenarioExecutor executor = new StandaloneScenarioExecutor(); RecursiveTestClass testClass = new RecursiveTestClass(); executor.injectSteps( testClass ); @@ -91,7 +89,7 @@ public void BeforeStage_methods_may_not_have_parameters() { expectedExceptionRule.expectMessage( "Could not execute method 'setup' of class 'BeforeStageWithParameters'" ); expectedExceptionRule.expectMessage( ", because it requires parameters" ); - ScenarioExecutor executor = new ScenarioExecutor(); + ScenarioExecutor executor = new StandaloneScenarioExecutor(); BeforeStageWithParameters stage = executor.addStage( BeforeStageWithParameters.class ); executor.startScenario( "Test" ); stage.something(); diff --git a/jgiven-junit/src/main/java/com/tngtech/jgiven/junit/ScenarioExecutionRule.java b/jgiven-junit/src/main/java/com/tngtech/jgiven/junit/ScenarioExecutionRule.java index 2f2d8ee7d7..81f01704cd 100644 --- a/jgiven-junit/src/main/java/com/tngtech/jgiven/junit/ScenarioExecutionRule.java +++ b/jgiven-junit/src/main/java/com/tngtech/jgiven/junit/ScenarioExecutionRule.java @@ -47,10 +47,10 @@ public ScenarioExecutionRule( ScenarioReportRule reportRule, Object testInstance } /** - * + * * @param testInstance the instance of the test class * @param scenario the scenario - * @since 0.7.0 + * @since 0.7.0 */ public ScenarioExecutionRule( Object testInstance, ScenarioBase scenario ) { this.testInstance = testInstance; @@ -103,7 +103,7 @@ protected void starting( Statement base, FrameworkMethod testMethod, Object targ ReportModelBuilder modelBuilder = scenario.getModelBuilder(); modelBuilder.setTestClass( testClass ); - scenario.getExecutor().startScenario( testMethod.getMethod(), getNamedArguments( base, testMethod, target ) ); + scenario.startScenario( testMethod.getMethod(), getNamedArguments( base, testMethod, target ) ); // inject state from the test itself scenario.getExecutor().readScenarioState( testInstance ); diff --git a/jgiven-spring/build.gradle b/jgiven-spring/build.gradle index 46809c9789..0a1e350667 100644 --- a/jgiven-spring/build.gradle +++ b/jgiven-spring/build.gradle @@ -7,7 +7,6 @@ dependencies { This behavior is not yet supported by Gradle, so this dependency has been converted to a compile dependency. Please review and delete this closure when resolved. */ } - - testCompile project(':jgiven-junit') + compile project(':jgiven-junit') testCompile group: 'org.springframework', name: 'spring-test', version: springVersion } diff --git a/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/JGivenStage.java b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/JGivenStage.java new file mode 100644 index 0000000000..9644a80fc0 --- /dev/null +++ b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/JGivenStage.java @@ -0,0 +1,29 @@ +package com.tngtech.jgiven.integration.spring; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.stereotype.Component; + +/** + * Annotation that marks a bean as JGiven Stage. + * + * @since 0.8.0 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface JGivenStage { + + /** + * The value may indicate a suggestion for a logical component name, + * to be turned into a Spring bean in case of an autodetected component. + * @return the suggested component name, if any + */ + String value() default ""; + +} diff --git a/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/JGivenStageAutoProxyCreator.java b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/JGivenStageAutoProxyCreator.java new file mode 100644 index 0000000000..0d2d14b510 --- /dev/null +++ b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/JGivenStageAutoProxyCreator.java @@ -0,0 +1,37 @@ +package com.tngtech.jgiven.integration.spring; + +import org.springframework.aop.TargetSource; +import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator; +import org.springframework.beans.BeansException; + +/** + * AutoProxyCreator that creates JGiven advices for all beans that + * are annotated with the {@link JGivenStage} annotation. See below + * on how to configure this bean. + * + *

+ * Sample configuration:
+ *

+ *   {@literal @}Bean
+ *   public JGivenStageAutoProxyCreator jGivenStageAutoProxyCreator() {
+ *       return new JGivenStageAutoProxyCreator();
+ *   }
+ *
+ * 
+ * @since 0.8.0 + */ +public class JGivenStageAutoProxyCreator extends AbstractAutoProxyCreator { + + private static final long serialVersionUID = 1L; + + @Override + protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, + String beanName, TargetSource customTargetSource) + throws BeansException { + if (beanClass.isAnnotationPresent(JGivenStage.class)) { + return new Object[] { getBeanFactory().getBean(SpringStepMethodInterceptor.class) }; + } + return DO_NOT_PROXY; + } + +} diff --git a/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringCanWire.java b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringCanWire.java index 62a5ffb202..828b03b497 100644 --- a/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringCanWire.java +++ b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringCanWire.java @@ -3,7 +3,10 @@ import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import com.tngtech.jgiven.integration.CanWire; - +/** + * @deprecated use SpringScenarioExecutor instead + */ +@Deprecated public class SpringCanWire implements CanWire { private final AutowireCapableBeanFactory factory; diff --git a/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringScenarioExecutor.java b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringScenarioExecutor.java new file mode 100644 index 0000000000..f9d15c62e0 --- /dev/null +++ b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringScenarioExecutor.java @@ -0,0 +1,67 @@ +package com.tngtech.jgiven.integration.spring; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.Advisor; +import org.springframework.aop.framework.Advised; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.util.ClassUtils; + +import com.tngtech.jgiven.impl.ScenarioExecutor; +import com.tngtech.jgiven.impl.StandaloneScenarioExecutor; + +/** + * Main class of JGiven for executing scenarios with spring support. + * See below on how to configure this bean. + *

+ * Sample Configuration: + *

+ *	{@literal @}Bean
+ *	{@literal @}Scope("prototype")
+ *	public SpringScenarioExecutor springScenarioExecutor() {
+ *	    return new SpringScenarioExecutor();
+ *	}
+ * 
+ *

+ * The SpringScenarioExecutor is stateful, and thus should use "prototype" scope + * @since 0.8.0 + */ +public class SpringScenarioExecutor extends StandaloneScenarioExecutor implements ScenarioExecutor { + + private static final Logger log = LoggerFactory.getLogger( SpringScenarioExecutor.class ); + + @Autowired + private ApplicationContext applicationContext; + + @Override + public T createStageClass( Class stepsClass ) { + try { + T bean = applicationContext.getBean(stepsClass); + Advised advised = (Advised)bean; + Advisor[] advisors = advised.getAdvisors(); + for (Advisor advisor : advisors) { + if (advisor.getAdvice() instanceof SpringStepMethodInterceptor ) { + SpringStepMethodInterceptor interceptor = (SpringStepMethodInterceptor)advisor.getAdvice(); + interceptor.setScenarioMethodHandler(this.methodHandler); + interceptor.setStackDepth(this.stackDepth); + interceptor.enableMethodHandling(true); + } + } + return bean; + } catch (NoSuchBeanDefinitionException nbe) { + return super.createStageClass(stepsClass); + } catch (ClassCastException cce) { + log.error("class " + ClassUtils.getShortName(stepsClass) + " is not advised with SpringStepMethodInterceptor. Falling back to cglib based proxy, strange things may happen."); + return super.createStageClass(stepsClass); + } + } + + @Override + public StageState getStageState( Object stage ) { + StageState stageState = stages.get( stage.getClass()); + return stageState != null ? stageState : super.getStageState(stage); + } + +} diff --git a/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringScenarioTest.java b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringScenarioTest.java new file mode 100644 index 0000000000..e38a353fb1 --- /dev/null +++ b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringScenarioTest.java @@ -0,0 +1,33 @@ +package com.tngtech.jgiven.integration.spring; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; + +import com.tngtech.jgiven.base.ScenarioTestBase; +import com.tngtech.jgiven.junit.ScenarioExecutionRule; +import com.tngtech.jgiven.junit.ScenarioReportRule; + +/** + * Base class for {@link SpringScenarioExecutor} based JGiven tests + * + * @param + * @param + * @param + * + * @since 0.8.0 + */ +public class SpringScenarioTest extends + ScenarioTestBase implements BeanFactoryAware { + + @ClassRule + public static final ScenarioReportRule writerRule = new ScenarioReportRule(); + + @Rule + public final ScenarioExecutionRule scenarioRule = new ScenarioExecutionRule( this, getScenario() ); + + public void setBeanFactory(BeanFactory beanFactory) { + getScenario().setExecutor(beanFactory.getBean(SpringScenarioExecutor.class)); + } +} diff --git a/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringStepMethodInterceptor.java b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringStepMethodInterceptor.java new file mode 100644 index 0000000000..f1e82a58c0 --- /dev/null +++ b/jgiven-spring/src/main/java/com/tngtech/jgiven/integration/spring/SpringStepMethodInterceptor.java @@ -0,0 +1,51 @@ +package com.tngtech.jgiven.integration.spring; + +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import com.tngtech.jgiven.impl.intercept.StepMethodInterceptor; + +/** + * StepMethodInterceptor that uses {@link MethodInterceptor} for intercepting JGiven methods + * See below on how to configure this bean. + * + *

+ * Sample Configuration: + *

+ * {@literal @}Bean
+ * {@literal @}Scope("prototype")
+ * public SpringStepMethodInterceptor springStepMethodInterceptor() {
+ *     return new SpringStepMethodInterceptor();
+ * }
+ * 
+ *

+ * The StepMethodInterceptor is stateful, and thus should use "prototype" scope + * @since 0.8.0 + */ +public class SpringStepMethodInterceptor extends StepMethodInterceptor implements MethodInterceptor { + + public SpringStepMethodInterceptor() { + super(null, null); + } + + @Override + public Object invoke(final MethodInvocation invocation) throws Throwable { + Object receiver = invocation.getThis(); + Method method = invocation.getMethod(); + Object[] parameters = invocation.getArguments(); + Invoker invoker = new Invoker() { + + @Override + public Object proceed() throws Throwable { + return invocation.proceed(); + } + }; + if (getScenarioMethodHandler() == null || getStackDepth() == null) { + return invoker.proceed(); // not running in JGiven context + } + return doIntercept(receiver, method, parameters, invoker); + } + +} diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/AnnotatedSpringScenarioTestTest.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/AnnotatedSpringScenarioTestTest.java new file mode 100644 index 0000000000..0bd34c0260 --- /dev/null +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/AnnotatedSpringScenarioTestTest.java @@ -0,0 +1,20 @@ +package com.tngtech.jgiven.integration.spring.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.tngtech.jgiven.integration.spring.SpringScenarioTest; + +@RunWith( SpringJUnit4ClassRunner.class ) +@ContextConfiguration( classes = TestSpringConfig.class ) +public class AnnotatedSpringScenarioTestTest extends SpringScenarioTest { + + @Test + public void spring_can_inject_beans_into_stages() { + given().a_stage_that_is_a_spring_component(); + when().methods_on_this_component_are_called(); + then().beans_are_injected(); + } +} diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/AnnotatedStage.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/AnnotatedStage.java new file mode 100644 index 0000000000..4bb86db11b --- /dev/null +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/AnnotatedStage.java @@ -0,0 +1,36 @@ +package com.tngtech.jgiven.integration.spring.test; + +import org.assertj.core.api.Assertions; +import org.springframework.beans.factory.annotation.Autowired; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; +import com.tngtech.jgiven.integration.spring.JGivenStage; +import com.tngtech.jgiven.integration.spring.JGivenStageAutoProxyCreator; + +/** + * example that uses {@link JGivenStage} to initialize a class as + * a spring bean and a JGiven stage (i.e. with automatically attached + * method interceptors) + * + * @see JGivenStageAutoProxyCreator + * + */ +@JGivenStage +public class AnnotatedStage extends Stage { + @Autowired + @ProvidedScenarioState + TestBean testBean; + + public AnnotatedStage a_stage_that_is_a_spring_component() { + return this; + } + + public AnnotatedStage methods_on_this_component_are_called() { + return this; + } + + public void beans_are_injected() { + Assertions.assertThat( testBean ).isNotNull(); + } +} diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/MixedStagesSpringScenarioTestTest.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/MixedStagesSpringScenarioTestTest.java new file mode 100644 index 0000000000..68a5cd7996 --- /dev/null +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/MixedStagesSpringScenarioTestTest.java @@ -0,0 +1,20 @@ +package com.tngtech.jgiven.integration.spring.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.tngtech.jgiven.integration.spring.SpringScenarioTest; + +@RunWith( SpringJUnit4ClassRunner.class ) +@ContextConfiguration( classes = TestSpringConfig.class ) +public class MixedStagesSpringScenarioTestTest extends SpringScenarioTest { + + @Test + public void using_beans_and_ordinary_stages_together() { + given().a_stage_that_is_a_spring_component(); + when().is_used_in_combination_with_ordinary_stages(); + then().mixing_them_works_as_expected(); + } +} diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SimpleTestSpringSteps.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SimpleTestSpringSteps.java new file mode 100644 index 0000000000..1183be670f --- /dev/null +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SimpleTestSpringSteps.java @@ -0,0 +1,37 @@ +package com.tngtech.jgiven.integration.spring.test; + +import org.assertj.core.api.Assertions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.tngtech.jgiven.integration.spring.JGivenStage; + +/** + * example for a Spring Bean that is used as a step + *

+ * note this bean neither inherits from {@link com.tngtech.jgiven.Stage} + * nor is annotated with the {@link JGivenStage} annotation, i.e. it is + * possible (but not recommended) to use unmodified already existing + * spring beans as stages. + *
+ * See {@link TestSpringConfig#jGivenBeanNameAutoProxyCreator()} on how to setup such beans. + * + */ +@Component +class SimpleTestSpringSteps { + + @Autowired + TestBean testBean; + + public SimpleTestSpringSteps a_step_that_is_a_spring_component() { + return this; + } + + public SimpleTestSpringSteps methods_on_this_component_are_called() { + return this; + } + + public void beans_are_injected() { + Assertions.assertThat( testBean ).isNotNull(); + } +} \ No newline at end of file diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SomeThen.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SomeThen.java new file mode 100644 index 0000000000..978c1f85ae --- /dev/null +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SomeThen.java @@ -0,0 +1,17 @@ +package com.tngtech.jgiven.integration.spring.test; + +import org.assertj.core.api.Assertions; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; + +public class SomeThen extends Stage { + + @ExpectedScenarioState + String result; + + public SomeThen mixing_them_works_as_expected() { + Assertions.assertThat(result).isEqualTo("result"); + return this; + } +} diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SomeWhen.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SomeWhen.java new file mode 100644 index 0000000000..674b804d23 --- /dev/null +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SomeWhen.java @@ -0,0 +1,20 @@ +package com.tngtech.jgiven.integration.spring.test; + +import com.tngtech.jgiven.Stage; +import com.tngtech.jgiven.annotation.ExpectedScenarioState; +import com.tngtech.jgiven.annotation.ProvidedScenarioState; + +public class SomeWhen extends Stage { + + @ExpectedScenarioState + TestBean testBean; + + @ProvidedScenarioState + String result; + + public SomeWhen is_used_in_combination_with_ordinary_stages() { + result = testBean.computeSomething(); + return this; + } + +} diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SpringScenarioTest.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SpringScenarioTest.java deleted file mode 100644 index 50d8e48298..0000000000 --- a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SpringScenarioTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.tngtech.jgiven.integration.spring.test; - -import org.assertj.core.api.Assertions; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; - -import com.tngtech.jgiven.integration.spring.SpringCanWire; -import com.tngtech.jgiven.integration.spring.test.SpringScenarioTest.SimpleTestSpringSteps; -import com.tngtech.jgiven.junit.SimpleScenarioTest; - -@RunWith( SpringJUnit4ClassRunner.class ) -@ContextConfiguration( classes = TestSpringConfig.class ) -public class SpringScenarioTest extends SimpleScenarioTest { - @Autowired - private AutowireCapableBeanFactory beanFactory; - - @Before - public void setupSpring() { - wireSteps( new SpringCanWire( beanFactory ) ); - } - - @Test - public void spring_can_inject_beans_into_stages() { - then().test_bean_is_injected(); - } - - static class SimpleTestSpringSteps { - @Autowired - TestBean testBean; - - public void test_bean_is_injected() { - Assertions.assertThat( testBean ).isNotNull(); - } - } -} diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SpringScenarioTestTest.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SpringScenarioTestTest.java new file mode 100644 index 0000000000..cd36a25d7e --- /dev/null +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/SpringScenarioTestTest.java @@ -0,0 +1,20 @@ +package com.tngtech.jgiven.integration.spring.test; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.tngtech.jgiven.integration.spring.SpringScenarioTest; + +@RunWith( SpringJUnit4ClassRunner.class ) +@ContextConfiguration( classes = TestSpringConfig.class ) +public class SpringScenarioTestTest extends SpringScenarioTest { + + @Test + public void spring_can_inject_beans_into_stages() { + given().a_step_that_is_a_spring_component(); + when().methods_on_this_component_are_called(); + then().beans_are_injected(); + } +} diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/TestBean.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/TestBean.java index a517a583a2..e2ad0c9aa5 100644 --- a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/TestBean.java +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/TestBean.java @@ -1,5 +1,11 @@ package com.tngtech.jgiven.integration.spring.test; +import org.springframework.stereotype.Component; + +@Component public class TestBean { + public String computeSomething() { + return "result"; + } } diff --git a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/TestSpringConfig.java b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/TestSpringConfig.java index 70d4f6940d..f4f4452d01 100644 --- a/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/TestSpringConfig.java +++ b/jgiven-spring/src/test/java/com/tngtech/jgiven/integration/spring/test/TestSpringConfig.java @@ -1,12 +1,50 @@ package com.tngtech.jgiven.integration.spring.test; +import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +import com.tngtech.jgiven.integration.spring.JGivenStageAutoProxyCreator; +import com.tngtech.jgiven.integration.spring.SpringScenarioExecutor; +import com.tngtech.jgiven.integration.spring.SpringStepMethodInterceptor; @Configuration +@ComponentScan(basePackages = "com.tngtech.jgiven.integration.spring.test") public class TestSpringConfig { + @Bean - public TestBean testBean() { - return new TestBean(); + @Scope("prototype") + public SpringStepMethodInterceptor springStepMethodInterceptor() { + return new SpringStepMethodInterceptor(); } + + @Bean + @Scope("prototype") + public SpringScenarioExecutor springScenarioExecutor() { + return new SpringScenarioExecutor(); + } + + /* + * configure support for {@link JGivenStage} annotation + */ + @Bean + public JGivenStageAutoProxyCreator jGivenStageAutoProxyCreator() { + return new JGivenStageAutoProxyCreator(); + } + + /* + * example for non-invasive usage of the {@link SpringStepMethodInterceptor} + * @return BeanNameAutoProxyCreator that proxies regular spring beans + */ + @Bean + public BeanNameAutoProxyCreator jGivenBeanNameAutoProxyCreator() { + BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator(); + beanNameAutoProxyCreator.setBeanNames(new String[]{"simpleTestSpringSteps"}); + beanNameAutoProxyCreator.setInterceptorNames(new String[]{"springStepMethodInterceptor"}); + return beanNameAutoProxyCreator; + } + + } diff --git a/jgiven-testng/src/main/java/com/tngtech/jgiven/testng/ScenarioTestListener.java b/jgiven-testng/src/main/java/com/tngtech/jgiven/testng/ScenarioTestListener.java index d149535708..f6b7d15280 100644 --- a/jgiven-testng/src/main/java/com/tngtech/jgiven/testng/ScenarioTestListener.java +++ b/jgiven-testng/src/main/java/com/tngtech/jgiven/testng/ScenarioTestListener.java @@ -41,7 +41,7 @@ public void onTestStart( ITestResult paramITestResult ) { scenario.setModel( scenarioCollectionModel ); Method method = paramITestResult.getMethod().getConstructorOrMethod().getMethod(); - scenario.getExecutor().startScenario( method, getArgumentsFrom( method, paramITestResult ) ); + scenario.startScenario( method, getArgumentsFrom( method, paramITestResult ) ); } @Override