Driving Web Behaviour

Selenium is a powerful open-source framework for automated web testing. From 2.x, it offers two APIs: the Selenium API (backwards compatible with Selenium 1.x) and the new WebDriver API. Both APIs are available from the same 2.x dependencies, so JBehave Web Selenium module supports both. When we talk about Selenium, we usually intend the framework, while if we need to distinguish between the APIs we'll specifically refer to the Selenium API and WebDriver API.

Let's see how we can easily use Selenium to run web-based stories. The objective in creating a DSL is to represent the business-domain functionality whilst abstracting away the details of the implementation, in this case the access to the web layer via a specific testing tool, such as Selenium. The same DSL should be re-usable with different testing tools.

As usual, examples speak louder than words:

Scenario: User searches for a single step
 
Given user is on Home page
When user opens Find Steps page
Then Find Steps page is shown
When user searches for "Given a threshold of 10.0"
Then search returns: "Given a threshold of $threshold"
When user views with methods
Then search returns: "Given a threshold of $threshold,
    [org.jbehave.web.examples.trader.steps.TraderSteps.aThreshold(double)]"
And steps instances include: "TraderSteps,StockExchangeSteps"

The TraderWebSteps class is just like any other JBehave steps class:

public class TraderWebSteps {
 
    private final Pages pages;
 
    public TraderWebSteps(Pages pages) {
        this.pages = pages;
    }
 
    @Given("user is on Home page")
    public void userIsOnHomePage(){       
        pages.home().open();       
    }
 
    @When("user opens Find Steps page")
    public void userClicksOnFindSteps(){       
        pages.findSteps().open();
    }
 
    @Then("Find Steps page is shown")
    public void findStepsPageIsShown(){
        pages.findSteps().pageIsShown();
    }
     
    @When("user searches for \"$step\"")
    public void userSearchesForSteps(String step){       
        pages.findSteps().find(step);
    }
 
    @Then("search returns: \"$stepsOrMethods\"")
    public void stepsFound(List<String> stepsOrMethods){  
        pages.findSteps().found(stepsOrMethods);
    }
 
    @Then("steps instances include: \"$names\"")
    public void stepsInstancesFound(List<String> names){  
        pages.findSteps().found(names);
    }
 
}

The new element here is that we are using Page Objects to abstract the Selenium behaviour behind pages that specify the user interaction in more meaningful ways. The page objects is where the Selenium magic happens. Note that the use of pages have so far enabled us not to commit to a specific API, either Selenium API or WebDriver API.

Configuring JBehave to use Selenium

As always, we configure JBehave to several different ways. One is to have an embeddable runnable class, e.g.the JUnit-runnable TraderWebStories. This is where the choice of API comes into play.

Using Selenium API

public class TraderWebStories extends JUnitStories {
   
    private Selenium selenium = SeleniumConfiguration.defaultSelenium();
    private ConditionRunner conditionRunner = SeleniumConfiguration.defaultConditionRunner(selenium);
    private Pages pages = new Pages(selenium, conditionRunner);
    private SeleniumSteps lifecycleSteps = new PerStorySeleniumSteps(selenium);
    private SeleniumContext seleniumContext = new SeleniumContext();
 
    @Override
    public Configuration configuration() {
        Class<? extends Embeddable> embeddableClass = this.getClass();
        return new SeleniumConfiguration()
            .useSelenium(selenium)
            .useSeleniumContext(seleniumContext)
            .useStepMonitor(new SeleniumStepMonitor(selenium, seleniumContext, new SilentStepMonitor()))
            .useStoryLoader(new LoadFromClasspath(embeddableClass))
            .useStoryReporterBuilder(new StoryReporterBuilder()
                .withCodeLocation(codeLocationFromClass(embeddableClass))
                .withDefaultFormats()
                .withFormats(CONSOLE, TXT, HTML, XML));
    }
 
    @Override
    public InjectableStepsFactory stepsFactory() {
        return new InstanceStepsFactory(configuration(),
                new TraderWebSteps(pages),
                lifecycleSteps,
                new SeleniumScreenshotOnFailure(selenium));
    }
     
    @Override
    protected List<String> storyPaths() {
        return new StoryFinder()
                .findPaths(codeLocationFromClass(this.getClass()).getFile(), asList("**/*.story"), null);
    }
 
}

Using WebDriver API

public class TraderWebStories extends JUnitStories {
   
    private WebDriverProvider driverProvider = new PropertyWebDriverProvider();
    private WebDriverSteps lifecycleSteps = new PerStoriesWebDriverSteps(driverProvider); // or PerStoryWebDriverSteps(driverProvider)
    private Pages pages = new Pages(driverProvider);
    private SeleniumContext context = new SeleniumContext();
    private ContextView contextView = new LocalFrameContextView().sized(500, 100);
     
    public TraderWebStories() {
        // If configuring lifecycle per-stories, you need to ensure that you a same-thread executor
        if ( lifecycleSteps instanceof PerStoriesWebDriverSteps ){
            configuredEmbedder().useExecutorService(MoreExecutors.sameThreadExecutor());
        }
    }
 
    @Override
    public Configuration configuration() {
        Class<? extends Embeddable> embeddableClass = this.getClass();
        return new SeleniumConfiguration()
                .useSeleniumContext(context)
                .useWebDriverProvider(driverProvider)
                .useStepMonitor(new SeleniumStepMonitor(contextView, context, new SilentStepMonitor()))
                .useStoryLoader(new LoadFromClasspath(embeddableClass))
                .useStoryReporterBuilder(new StoryReporterBuilder()
                    .withCodeLocation(codeLocationFromClass(embeddableClass))
                    .withDefaultFormats()
                    .withFormats(CONSOLE, TXT, HTML, XML));
    }
 
    @Override
    public InjectableStepsFactory stepsFactory() {
        Configuration configuration = configuration();
        return new InstanceStepsFactory(configuration,
                new TraderWebSteps(pages),
                lifecycleSteps,
                new WebDriverScreenshotOnFailure(driverProvider, configuration.storyReporterBuilder()));
    }
 
 
    @Override
    protected List<String> storyPaths() {
        return new StoryFinder()
                .findPaths(codeLocationFromClass(this.getClass()).getFile(), asList("**/*.story"), null);
    }
 
    // This Embedder is used by Maven or Ant and it will override anything set in the constructor
    public static class SameThreadEmbedder extends Embedder {
         
        public SameThreadEmbedder() {
            useExecutorService(MoreExecutors.sameThreadExecutor());
        }
 
    }
 
}

Lifecycle Steps

JBehave integration with Selenium and WebDriver APIs aims to facilitate common tasks. Amongst these, one of the most common is the management of the lifecycle, e.g. starting and stopping the browser.

JBehave provides three type of lifecycle management:

  1. per-stories: via the PerStoriesSeleniumSteps or PerStoriesWebDriverSteps.
  2. per-story: via the PerStorySeleniumSteps or PerStoryWebDriverSteps.
  3. per-scenario: via the PerScenarioSeleniumSteps or PerScenarioWebDriverSteps.
If you are using per-stories lifecycle, you must ensure that you are using a same-thread executor. C.f. the trader-webdriver example on how to configure the use of a same-thread executor.

Running Selenium-based tests in an automated way

NOTE: To get the Selenium based tests running in an automated way, you need to run both a webapp server, e.g. Jetty, and the Selenium server (only for the Selenium API, as the WebDriver API does not need it). See the trader runner selenium example and trader runner webdriver example for a way to do this using Maven.