Friday, May 31, 2013

How to Scope Scenarios with JBehave and Guice

If you use JBehave with a dependency injection framework such as Spring or Guice, you may quickly realize that scoping the Step classes gets a little tricky.  By default, both of those frameworks provide two basic scopes: instance (or prototype) scope, and singleton scope.

Neither one of these options is great for use with JBehave.  Instance scope is a problem because JBehave then creates a new Step class for every step in a scenario, making it impossible to share state between steps without using static (global) state.  Singleton scope is a different side of the same problem: state ends up shared among all scenarios.  In either case, to make things work you must remember to clean up the global state after each scenario.

A simpler solution would be to implement a custom "scenario" scope.  I will show you how to do this for Guice below.

First, we need to define a new custom scope by implementing Guice's Scope interface.  This class will be a container that adds and manages our dependencies when the scope is entered, and removes them when the scope is exited.  This could be potentially daunting and error prone, but fortunately the Guice developers have provided us with a default called SimpleScope, which does all this for us.  This class is sufficient for our needs, and you can copy it as-is straight into your source code.

Second, we need to tell JBehave when to actually create a new scope, and when to close the scope out.  Since we want to scope our dependencies to scenarios, we use JBehave's @BeforeScenario and @AfterScenario annotations to enter and exit each scope.  Note that we must inject our copy of SimpleScope, which is what actually manages the scoped dependencies.

 public class ScenarioContext {  
   
   private SimpleScope scope;  
   
   @Inject  
   public ScenarioContext( @Named ( "scenarioScope" ) SimpleScope scope ) {  
     this.scope = scope;
   }  
   
   @BeforeScenario  
   public void beforeScenario() {  
     scope.enter();  
   }  
   
   @AfterScenario  
   public void afterScenario() {  
     scope.exit();  
   }  
 }  

Third, much like the Singleton annotation, we need a binding annotation to inform Guice about how we'd like our step classes scoped.  We will use this new annotation to bind instances to our new SimpleScope class.  We create it simply like thus:

  import static java.lang.annotation.ElementType.METHOD;   
  import static java.lang.annotation.ElementType.TYPE;   
  import static java.lang.annotation.RetentionPolicy.RUNTIME;   
     
  import java.lang.annotation.Retention;   
  import java.lang.annotation.Target;   
     
  import com.google.inject.ScopeAnnotation;   
     
  @Target ( { TYPE, METHOD } )   
  @Retention ( RUNTIME )   
  @ScopeAnnotation   
  public @interface ScenarioScope {}   

There are two parts to the final step.  The first is to actually bind our step classes to our new scope, which is accomplished simply by providing the class file to the binder using .in().  However, we also need to inform Guice about how to manage the SimpleScope container.

 public class AppModule extends AbstractModule {  
   @Override  
   protected void configure() {  
     setUpScenarioScope();  
   
     bind( MySteps.class ).in( ScenarioScope.class );  
   }  
   
   private void setUpScenarioScope() {  
   
     bindScope( ScenarioScope.class, scenarioScope );  

     SimpleScope scenarioScope = new SimpleScope();  
     bind( SimpleScope.class ).annotatedWith( Names.named( "scenarioScope" ) ).toInstance( scenarioScope );  
     bind( ScenarioContext.class ).in( Singleton.class );  
   }  
 }  

The setUpScenarioScope() method above does a couple of things:
  • informs Guice of our new scope, using bindScope()
  • creates an instance of our SimpleScope class for managing dependencies (we only need one)
  • ensures that instance can be injected into our JBehave-annotated context class
  • binds that context in the singleton scope
That's it!  All steps annotated for scenario scope will be able to share data within a single step class, while  guaranteeing a fresh set of steps for every new scenario.

Known issue: This approach is not currently compatible with jbehave-junit-runner library.  That library creates a special JBehave runner which formats the test results in a standard JUnit output, and it relies on a older copy of JBehave that causes a chicken-and-egg problem with Step creation.  A patch has been submitted to fix this, but to date it has not been incorporated into a release.  A workaround is to build from source and apply the patch manually, and make sure you are using JBehave 3.8+.