Now that I’m using JBehave in a commercial project, I’ve rewritten the loading of the scenario files in such a way that I can call my tests something like com.example.login.InvalidLoginScenario and have the corresponding scenario file under {project}/src/test/resources/invalid_login.scenario.
Previously…
The standard JBehave scenario file is loaded with UnderscoredCamelCaseResolver, which converts the classname from camel-case to underscore-seperated classname. A resource path is constructed from the package plus the underscored filename to locate the file – e.g. {src.test}/com/example/login/invalid_login_scenario.
Previously (with inspiration), I modified the testcase to override the default Configuration object, which allowed the loading of the scenario file with an extension – so it would now look for {src.test}/com/example/login/invalid_login_scenario.scenario.
Goal
To make the creation and maintenance of the JBehave scenarios and testcases easier, I decided on some standards:
- JBehave testcase classes should be suffixed with ..Scenario, to clearly communicate their purpose.
- Scenario filenames should map to their corresponding test classes.
- Scenario files should reside in the same location under resources/.
- Scenario files should have the extension .scenario, to improve readability.
But instead of having a file named invalid_login_scenario.scenario, I want the test class InvalidLoginScenario to map to the file invalid_login.scenario. All this was basically possible with existing JBehave classes, when configured the correct way (and certain functions overridden).
The Source
import org.jbehave.scenario.PropertyBasedConfiguration; import org.jbehave.scenario.RunnableScenario; import org.jbehave.scenario.errors.PendingErrorStrategy; import org.jbehave.scenario.parser.ClasspathScenarioDefiner; import org.jbehave.scenario.parser.PatternScenarioParser; import org.jbehave.scenario.parser.ScenarioDefiner; import org.jbehave.scenario.parser.UnderscoredCamelCaseResolver; /** * Customisation of standard JBehave {@link PropertyBasedConfiguration} to allow clearer naming of scenario files: * <ul> * <li> Usage of *.scenario file extension</li> * <li> Strip 'Scenario' off test class names</li> * <li> Load scenario files from classpath root</li> * </ul> * So a test class named <code>InvalidUsernameScenario</code> would be attempting to resolve the resource path <code>/invalid_username.scenario</code>. * * This configuration also fails on 'pending' (unimplemented) steps. */ public final class ScenarioConfiguration extends PropertyBasedConfiguration { private final ClassLoader _classLoader; public ScenarioConfiguration(final ClassLoader classLoader) { _classLoader = classLoader; } @Override public ScenarioDefiner forDefiningScenarios() { final ResourceNameResolver filenameResolver = new ResourceNameResolver(".scenario"); filenameResolver.removeFromClassname("Scenario"); return new ClasspathScenarioDefiner(filenameResolver, new PatternScenarioParser(this.keywords()), _classLoader); } @Override public PendingErrorStrategy forPendingSteps() { return PendingErrorStrategy.FAILING; } /** * Override {@link UnderscoredCamelCaseResolver} to load resources from classpath root. This means we can collect * scenario files in a single resource directory instead of in packages. */ class ResourceNameResolver extends UnderscoredCamelCaseResolver { public ResourceNameResolver(final String extension) { super(extension); } @java.lang.Override protected String resolveDirectoryName(final Class<? extends RunnableScenario> scenarioClass) { return ""; } } }
The function forDefiningScenarios() is the important part – it sets the resolver to use the .scenario extension, but also strips out ‘Scenario’ from the class name.
Also, to force the resolver to look at the classpath root, the resolveDirectoryName() function is overridden to return an empty string.
The testcases using this Configuration object would call:
public class InvalidLoginScenario extends Scenario { public InvalidLoginScenario() { super(new ScenarioConfiguration(InvalidLoginScenario.class.getClassLoader()), new LoginScenarioSteps()); } }