Skip to content

Commit

Permalink
Added required attribute to ScenarioState.
Browse files Browse the repository at this point in the history
This commit adds a new boolean attribute "required" to both
ScenarioState and ExpectedScenarioState. If set to true on a
field within a stage, corresponding tests will fail automatically
if the state hasn't been provided.

fixes TNG#255
  • Loading branch information
Airblader committed Dec 24, 2016
1 parent a923659 commit 853f6c2
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@
@Target( ElementType.FIELD )
public @interface ExpectedScenarioState {
Resolution resolution() default Resolution.AUTO;

/**
* Marks this state as required for the stage. If in this case the state isn't provided, a
* {@code JGivenMissingRequiredScenarioStateException} will be thrown.
*
* @since 0.14.0
*/
boolean required() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@
public enum Resolution {
TYPE, NAME, AUTO;
}

/**
* Marks this state as required for the stage. If in this case the state isn't provided, a
* {@code JGivenMissingRequiredScenarioStateException} will be thrown.
*
* @since 0.14.0
*/
boolean required() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.tngtech.jgiven.exception;

import java.lang.reflect.Field;

/**
* This exception is thrown if a scenario state has been marked as required,
* but the state hasn't been provided.
*/
public class JGivenMissingRequiredScenarioStateException extends RuntimeException {

private static final long serialVersionUID = 1L;

public JGivenMissingRequiredScenarioStateException( Field field ) {
super( "The field " + field.getName() + " is required but has not been provided." );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.tngtech.jgiven.impl.inject;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;

import com.tngtech.jgiven.annotation.ExpectedScenarioState;
import com.tngtech.jgiven.annotation.ProvidedScenarioState;
import com.tngtech.jgiven.annotation.ScenarioState;
import com.tngtech.jgiven.annotation.ScenarioState.Resolution;
import com.tngtech.jgiven.relocated.guava.base.Function;

/**
* Used internally to avoid repeated annotation lookups.
*/
final class ScenarioStateField {

public static Function<Field, ScenarioStateField> fromField = new Function<Field, ScenarioStateField>() {
@Override
public ScenarioStateField apply( Field field ) {
return new ScenarioStateField( field );
}
};

private final Field field;

private Resolution declaredResolution;
private boolean required;

private ScenarioStateField( Field field ) {
this.field = field;

collectAnnotations( field );
if( declaredResolution == null ) {
throw new IllegalArgumentException( "Field " + field + " has no valid annotation" );
}
}

public Field getField() {
return field;
}

/**
* Return the {@link Resolution} defined for this state.
*/
public Resolution getResolution() {
if( declaredResolution == Resolution.AUTO ) {
return typeIsTooGeneric( field.getType() ) ? Resolution.NAME : Resolution.TYPE;
}

return declaredResolution;
}

/**
* Returns {@code true} if and only if the {@link Required} annotation is present on this state.
*/
public boolean isRequired() {
return required;
}

private void collectAnnotations( Field field ) {
for( Annotation annotation : field.getAnnotations() ) {
if( declaredResolution == null ) {
declaredResolution = collectDeclaredResolution( annotation );
}

required |= collectRequired( annotation );
}
}

private Resolution collectDeclaredResolution( Annotation annotation ) {
if( annotation instanceof ScenarioState ) {
return ( (ScenarioState) annotation ).resolution();
}

if( annotation instanceof ProvidedScenarioState ) {
return ( (ProvidedScenarioState) annotation ).resolution();
}

if( annotation instanceof ExpectedScenarioState ) {
return ( (ExpectedScenarioState) annotation ).resolution();
}

return null;
}

private boolean collectRequired( Annotation annotation ) {
if( annotation instanceof ScenarioState ) {
return ( (ScenarioState) annotation ).required();
}

if( annotation instanceof ExpectedScenarioState ) {
return ( (ExpectedScenarioState) annotation ).required();
}

return false;
}

private boolean typeIsTooGeneric( Class<?> type ) {
return type.isPrimitive()
|| type.getName().startsWith( "java.lang" )
|| type.getName().startsWith( "java.io" )
|| type.getName().startsWith( "java.util" );
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.tngtech.jgiven.impl.inject;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
Expand All @@ -9,13 +8,15 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.tngtech.jgiven.relocated.guava.collect.Maps;
import com.tngtech.jgiven.annotation.ExpectedScenarioState;
import com.tngtech.jgiven.annotation.ProvidedScenarioState;
import com.tngtech.jgiven.annotation.ScenarioState;
import com.tngtech.jgiven.annotation.ScenarioState.Resolution;
import com.tngtech.jgiven.exception.AmbiguousResolutionException;
import com.tngtech.jgiven.exception.JGivenMissingRequiredScenarioStateException;
import com.tngtech.jgiven.impl.util.FieldCache;
import com.tngtech.jgiven.relocated.guava.collect.Lists;
import com.tngtech.jgiven.relocated.guava.collect.Maps;

/**
* Used by Scenario to inject and read values from objects.
Expand All @@ -42,57 +43,65 @@ public void validateFields( Object object ) {

Map<Object, Field> resolvedFields = Maps.newHashMap();

for( Field field : getScenarioFields( object ) ) {
field.setAccessible( true );
Resolution resolution = getResolution( field );
for( ScenarioStateField field : getScenarioFields( object ) ) {
field.getField().setAccessible( true );
Resolution resolution = field.getResolution();
Object key = null;
if( resolution == Resolution.NAME ) {
key = field.getName();
key = field.getField().getName();
} else {
key = field.getType();
key = field.getField().getType();
}
if( resolvedFields.containsKey( key ) ) {
Field existingField = resolvedFields.get( key );
throw new AmbiguousResolutionException( "Ambiguous fields with same " + resolution + " detected. Field 1: " +
existingField + ", field 2: " + field );
existingField + ", field 2: " + field.getField() );
}
resolvedFields.put( key, field );
resolvedFields.put( key, field.getField() );
}

validatedClasses.put( object.getClass(), Boolean.TRUE );
}

private List<Field> getScenarioFields( Object object ) {
return FieldCache.get( object.getClass() ).getFieldsWithAnnotation( ScenarioState.class, ProvidedScenarioState.class,
ExpectedScenarioState.class );
private List<ScenarioStateField> getScenarioFields( Object object ) {
@SuppressWarnings( "unchecked" )
List<Field> scenarioFields = FieldCache
.get( object.getClass() )
.getFieldsWithAnnotation( ScenarioState.class, ProvidedScenarioState.class, ExpectedScenarioState.class );

return Lists.transform( scenarioFields, ScenarioStateField.fromField );
}

@SuppressWarnings( "unchecked" )
public void readValues( Object object ) {
validateFields( object );
for( Field field : getScenarioFields( object ) ) {
for( ScenarioStateField field : getScenarioFields( object ) ) {
try {
Object value = field.get( object );
Object value = field.getField().get( object );
updateValue( field, value );
log.debug( "Reading value {} from field {}", value, field );
log.debug( "Reading value {} from field {}", value, field.getField() );
} catch( IllegalAccessException e ) {
throw new RuntimeException( "Error while reading field " + field, e );
throw new RuntimeException( "Error while reading field " + field.getField(), e );
}
}
}

@SuppressWarnings( "unchecked" )
public void updateValues( Object object ) {
validateFields( object );
for( Field field : getScenarioFields( object ) ) {
try {
Object value = getValue( field );
if( value != null ) {
field.set( object, value );
log.debug( "Setting field {} to value {}", field, value );
for( ScenarioStateField field : getScenarioFields( object ) ) {
Object value = getValue( field );

if( value != null ) {
try {
field.getField().set( object, value );
} catch( IllegalAccessException e ) {
throw new RuntimeException( "Error while updating field " + field.getField(), e );
}
} catch( IllegalAccessException e ) {
throw new RuntimeException( "Error while updating field " + field, e );

log.debug( "Setting field {} to value {}", field.getField(), value );
} else if( field.isRequired() ) {
throw new JGivenMissingRequiredScenarioStateException( field.getField() );
}
}
}
Expand All @@ -105,55 +114,20 @@ public <T> void injectValueByName( String name, T value ) {
state.updateValueByName( name, value );
}

private void updateValue( Field field, Object value ) {
Resolution resolution = getResolution( field );
Class<?> type = field.getType();
if( resolution == Resolution.NAME ) {
String name = field.getName();
state.updateValueByName( name, value );
private void updateValue( ScenarioStateField field, Object value ) {
if( field.getResolution() == Resolution.NAME ) {
state.updateValueByName( field.getField().getName(), value );
} else {
state.updateValueByType( type, value );
}
}

private Object getValue( Field field ) {
Resolution resolution = getResolution( field );
Class<?> type = field.getType();
if( resolution == Resolution.NAME ) {
String name = field.getName();
return state.getValueByName( name );
}
return state.getValueByType( type );
}

private Resolution getResolution( Field field ) {
Resolution resolution = getDeclaredResolution( field );
if( resolution == Resolution.AUTO ) {
return typeIsTooGeneric( field.getType() ) ? Resolution.NAME : Resolution.TYPE;
state.updateValueByType( field.getField().getType(), value );
}
return resolution;
}

private Resolution getDeclaredResolution( Field field ) {
for( Annotation annotation : field.getAnnotations() ) {
if( annotation instanceof ScenarioState ) {
return ( (ScenarioState) annotation ).resolution();
}
if( annotation instanceof ProvidedScenarioState ) {
return ( (ProvidedScenarioState) annotation ).resolution();
}
if( annotation instanceof ExpectedScenarioState ) {
return ( (ExpectedScenarioState) annotation ).resolution();
}
private Object getValue( ScenarioStateField field ) {
if( field.getResolution() == Resolution.NAME ) {
return state.getValueByName( field.getField().getName() );
}
throw new IllegalArgumentException( "Field " + field + " has no valid annotation" );
}

private boolean typeIsTooGeneric( Class<?> type ) {
return type.isPrimitive()
|| type.getName().startsWith( "java.lang" )
|| type.getName().startsWith( "java.io" )
|| type.getName().startsWith( "java.util" );
return state.getValueByType( field.getField().getType() );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.tngtech.jgiven.junit;

import org.junit.Test;
import org.junit.runner.RunWith;

import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.jgiven.annotation.ExpectedScenarioState;
import com.tngtech.jgiven.annotation.JGivenConfiguration;
import com.tngtech.jgiven.annotation.ScenarioState;
import com.tngtech.jgiven.exception.JGivenMissingRequiredScenarioStateException;
import com.tngtech.jgiven.junit.test.BeforeAfterTestStage;
import com.tngtech.jgiven.junit.test.ThenTestStep;
import com.tngtech.jgiven.junit.test.WhenTestStep;

@RunWith( DataProviderRunner.class )
@JGivenConfiguration( TestConfiguration.class )
public class RequiredScenarioStateTest extends ScenarioTest<BeforeAfterTestStage, WhenTestStep, ThenTestStep> {

static class StageWithMissingScenarioState {
@ScenarioState( required = true )
Boolean state;

public void something() {}
}

@Test( expected = JGivenMissingRequiredScenarioStateException.class )
public void required_states_must_be_present() throws Throwable {
StageWithMissingScenarioState stage = addStage( StageWithMissingScenarioState.class );
stage.something();
}

static class StageWithMissingExpectedScenarioState {
@ExpectedScenarioState( required = true )
Boolean state;

public void something() {}
}

@Test( expected = JGivenMissingRequiredScenarioStateException.class )
public void required__expected_states_must_be_present() throws Throwable {
StageWithMissingExpectedScenarioState stage = addStage( StageWithMissingExpectedScenarioState.class );
stage.something();
}

static class ProviderStage {
@ScenarioState
Boolean state;

public void provide() {
this.state = true;
}
}

@Test
public void scenarios_pass_if_required_state_is_provided_by_another_stage() throws Throwable {
ProviderStage stage = addStage( ProviderStage.class );
StageWithMissingScenarioState stage2 = addStage( StageWithMissingScenarioState.class );

stage.provide();
stage2.something();
}

}

0 comments on commit 853f6c2

Please sign in to comment.