Behaviour-Driven Development encourages you to start defining the stories via scenarios that express the desired behaviour in a textual format, e.g.:
Given a stock of symbol STK1 and a threshold of 10.0 When the stock is traded at 5.0 Then the alert status should be OFF |
The textual scenario should use the language of the business domain and shield away as much as possible the details of the technical implementation. Also, it should be given a name that is expressive of the functionality that is being verified, i.e. trader_is_alerted_of_status.story.
The scenario should use a syntax compatible with the Grammar.
A story is a collection of scenarios, each detailing different examples of the behaviour of a given increment of functionality of the system.Scenario : trader is not alerted below threshold Given a stock of symbol STK1 and a threshold of 10.0 When the stock is traded at 5.0 Then the alert status should be OFF Scenario : trader is alerted above threshold Given a stock of symbol STK1 and a threshold of 10.0 When the stock is traded at 11.0 Then the alert status should be ON |
JBehave maps textual steps to Java methods via CandidateSteps. The scenario writer need only provide annotated methods that match, by regex patterns, the textual steps.
public class TraderSteps { // look, Ma, I'm a POJO!! private Stock stock; @Given ( "a stock of symbol $symbol and a threshold of $threshold" ) public void stock(String symbol, double threshold) { stock = new Stock(symbol, threshold); } @When ( "the stock is traded at $price" ) public void theStockIsTradedAt( double price) { stock.tradeAt(price); } @Then ( "the alert status should be $status" ) public void theAlertStatusShouldBe(String status) { ensureThat(stock.getStatus().name(), equalTo(status)); } } |
To create instances of CandidateSteps we use an instance of InjectableStepsFactory
Configuration configuration = ... // optional configuration InjectableStepsFactory factory = new InstanceStepsFactory(configuration, new TraderSteps(), new BeforeAndAfterSteps()); |
Each step is annotated with one of the step annotations, each holding a regex pattern as value. The pattern is used to match the method in the Steps class with the appropriate parameters. The simplest default behaviour identifies arguments in the candidate step by the words prefixed by the $ character. More advanced parameter injection mechanisms are also supported by JBehave.
JBehave execute all the matched steps in the order in which they are found in the scenario. It is up to the implementor of the Steps classes to provide the logic to tie together the results of the execution of each step. This can be done by keeping state member variables in the Steps class or possibly by using a service API or other dependency.
At the heart of the JBehave running of stories lies the Embedder, which provides an entry point to all of JBehave's functionality that is embeddable into other launchers, such as IDEs or CLIs. JBehave complements the Embedder with an Embeddable which represents a runnable facade to the Embedder.
JBehave allows many different ways to configure Embeddable Java classes that allow the parsing and running of textual stories.JBehave provides two main Embeddable implementations:
JUnit is supported out-of-the-box via several Embeddables implementations:
JUnitReportingRunner can be used to visualize the structure and the successes/failures
In the case of one-to-one mapping, our abstract base TraderStory would look like:
@RunWith (JUnitReportingRunner. class ) public abstract class TraderStory extends JUnitStory { public TraderStory() { configuredEmbedder().embedderControls().doGenerateViewAfterStories( true ).doIgnoreFailureInStories( true ) .doIgnoreFailureInView( true ).useThreads( 2 ).useStoryTimeoutInSecs( 60 ); // Uncomment to set meta filter, which can also be set via Maven // configuredEmbedder().useMetaFilters(Arrays.asList("+theme parametrisation")); } @Override public Configuration configuration() { Class<? extends Embeddable> embeddableClass = this .getClass(); Properties viewResources = new Properties(); viewResources.put( "decorateNonHtml" , "true" ); // Start from default ParameterConverters instance ParameterConverters parameterConverters = new ParameterConverters(); // factory to allow parameter conversion and loading from external // resources (used by StoryParser too) ExamplesTableFactory examplesTableFactory = new ExamplesTableFactory( new LocalizedKeywords(), new LoadFromClasspath(embeddableClass), parameterConverters); // add custom converters parameterConverters.addConverters( new DateConverter( new SimpleDateFormat( "yyyy-MM-dd" )), new ExamplesTableConverter(examplesTableFactory)); return new MostUsefulConfiguration() .useStoryControls( new StoryControls().doDryRun( false ).doSkipScenariosAfterFailure( false )) .useStoryLoader( new LoadFromClasspath(embeddableClass)) .useStoryParser( new RegexStoryParser(examplesTableFactory)) .useStoryPathResolver( new UnderscoredCamelCaseResolver()) .useStoryReporterBuilder( new StoryReporterBuilder() .withCodeLocation(CodeLocations.codeLocationFromClass(embeddableClass)) .withDefaultFormats().withPathResolver( new ResolveToPackagedName()) .withViewResources(viewResources).withFormats(CONSOLE, TXT, HTML, XML) .withFailureTrace( true ).withFailureTraceCompression( true )) .useParameterConverters(parameterConverters)); } @Override public InjectableStepsFactory stepsFactory() { return new InstanceStepsFactory(configuration(), new TraderSteps( new TradingService()), new AndSteps(), new MetaParametrisationSteps(), new CalendarSteps(), new PriorityMatchingSteps(), new PendingSteps(), new ParametrisedSteps(), new SandpitSteps(), new SearchSteps(), new BeforeAfterSteps(), new CompositeSteps(), new CompositeNestedSteps(), new NamedParametersSteps()); } } |
We then extend this base class with a class for each story, e.g.
TraderIsAletedOfStatus.java
, which maps to out textual
story in same package.
In the case of many-to-one mapping:
@RunWith (JUnitReportingRunner. class ) public class TraderStories extends JUnitStories { public TraderStories() { configuredEmbedder().embedderControls().doGenerateViewAfterStories( true ).doIgnoreFailureInStories( false ) .doIgnoreFailureInView( true ).doVerboseFailures( true ).useThreads( 2 ).useStoryTimeoutInSecs( 60 ); //configuredEmbedder().useEmbedderControls(new PropertyBasedEmbedderControls()); } @Override public Configuration configuration() { Class<? extends Embeddable> embeddableClass = this .getClass(); Properties viewResources = new Properties(); viewResources.put( "decorateNonHtml" , "true" ); viewResources.put( "reports" , "ftl/jbehave-reports-with-totals.ftl" ); // Start from default ParameterConverters instance ParameterConverters parameterConverters = new ParameterConverters(); // factory to allow parameter conversion and loading from external resources (used by StoryParser too) ExamplesTableFactory examplesTableFactory = new ExamplesTableFactory( new LocalizedKeywords(), new LoadFromClasspath(embeddableClass), parameterConverters); // add custom converters parameterConverters.addConverters( new DateConverter( new SimpleDateFormat( "yyyy-MM-dd" )), new ExamplesTableConverter(examplesTableFactory)); return new MostUsefulConfiguration() .useStoryLoader( new LoadFromClasspath(embeddableClass)) .useStoryParser( new RegexStoryParser(examplesTableFactory)) .useStoryReporterBuilder( new StoryReporterBuilder() .withCodeLocation(CodeLocations.codeLocationFromClass(embeddableClass)) .withDefaultFormats() .withViewResources(viewResources) .withFormats(CONSOLE, TXT, HTML_TEMPLATE, XML_TEMPLATE) .withFailureTrace( true ) .withFailureTraceCompression( true )) .useParameterConverters(parameterConverters)); } @Override public InjectableStepsFactory stepsFactory() { return new InstanceStepsFactory(configuration(), new TraderSteps( new TradingService()), new AndSteps(), new MetaParametrisationSteps(), new CalendarSteps(), new PriorityMatchingSteps(), new PendingSteps(), new SandpitSteps(), new SearchSteps(), new BeforeAfterSteps(), new CompositeSteps(), new NamedParametersSteps()); } @Override protected List<String> storyPaths() { // Specify story paths as URLs String codeLocation = codeLocationFromClass( this .getClass()).getFile(); return new StoryFinder().findPaths(codeLocation, asList( "**/trader_is_alerted_of_status.story" , "**/traders_can_be_subset.story" ), asList( "" ), "file:" + codeLocation); } } |
Usually, this is everything you have to do. If you want to, you can use a little helper method to configure the configured Embedder so that it works nicely together with the JUnitReportingRunner.
JUnitReportingRunner.recommendedControls(configuredEmbedder()); |
This tells JBehave to don't throw any exceptions when generating views or otherwise wrapping the execution up. This would confuse JUnit considerably, rendering the AfterStories Suite incomplete.
Once you are all set, you use the regular Run As -> JUnit Test command in Eclipse or similar command in IntelliJ IDEA. The JUnit view should appear and display something like this:
JBehave also provides an implementation of JUnit's Runner, AnnotatedEmbedderRunner, which is runnable via JUnit's @RunWith annotation:
@RunWith (AnnotatedEmbedderRunner. class ) @Configure (storyLoader = MyStoryLoader. class , storyReporterBuilder = MyReportBuilder. class , parameterConverters = { MyDateConverter. class }) @UsingEmbedder (embedder = Embedder. class , generateViewAfterStories = true , ignoreFailureInStories = true , ignoreFailureInView = true ) @UsingSteps (instances = { TraderSteps. class , BeforeAfterSteps. class , AndSteps. class , CalendarSteps. class , PriorityMatchingSteps. class , SandpitSteps. class }) public class TraderAnnotatedEmbedder implements Embeddable { private Embedder embedder; public void useEmbedder(Embedder embedder) { this .embedder = embedder; } @Test public void run() { embedder.runStoriesAsPaths( new StoryFinder().findPaths(codeLocationFromClass( this .getClass()).getFile(), asList( "**/*.story" ), asList( "" ))); } |
As remarked above, JBehave does not impose any tie-in with any framework to run stories. It only requires access to the Embedder to run the stories. The following snippet shows, for example, how to use SpringJUnit4ClassRunner to compose and inject steps instances and them the stories:
@RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (locations = { "/org/jbehave/examples/trader/spring/steps.xml" }) public class AnnotatedEmbedderWithSpringJUnit4ClassRunner { @Autowired private TraderSteps traderSteps; @Autowired private BeforeAfterSteps beforeAndAfterSteps; @Test public void runStoriesAsPaths() { embedder().runStoriesAsPaths(storyPaths()); } @Test public void findMatchingCandidateSteps() { embedder().reportMatchingStepdocs( "When traders are subset to \".*y\" by name" ); embedder().reportMatchingStepdocs( "Given a step that is not matched" ); } private Embedder embedder() { Embedder embedder = new ClasspathTraderEmbedder(); embedder.useStepsFactory( new InstanceStepsFactory(embedder.configuration(), traderSteps, beforeAndAfterSteps)); return embedder; } protected List<String> storyPaths() { StoryFinder finder = new StoryFinder(); return finder.findPaths(codeLocationFromClass( this .getClass()).getFile(), asList( "**/*.story" ), asList( "" )); } } |