Integrating Gwen with Maven

Although you can download and install Gwen locally to run automated tests, it is sometimes useful to integrate it with a build tool such as Maven and have it download and install it for you and run tests as part of the release verification process. Integrating with Maven can also make it easier for you to run Gwen on a continuous integration build server too.

Gwen does not ship with a Maven plugin, but you can still integrate it with Maven using a POM file like this:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>org.gweninterpreter</groupId>
  <artifactId>gwen-web-maven</artifactId>
  <name>gwen-web-maven</name>
  <version>4.0.0</version>
  <packaging>pom</packaging>

  <profiles>
    <profile>
      <id>gwen</id>
      <activation>
        <property>
          <name>gwen.args</name>
        </property>
      </activation>
      <properties>
        <gwen.web.version>???</gwen.web.version>
        <!--selenium.version>???</selenium.version-->
        <!--selenium.dir>target/selenium-${selenium.version}</selenium.dir-->
        <gwen.dir>target/gwen-web-${gwen.web.version}</gwen.dir>
        <gwen.classpath>${selenium.dir}/*${path.separator}${gwen.dir}/lib/*</gwen.classpath>
      </properties>
      <dependencies>
        <!--dependency>
          <groupId>org.seleniumhq.selenium</groupId>
          <artifactId>selenium-java</artifactId>
          <version>${selenium.version}</version>
        </dependency-->
        <dependency>
          <groupId>org.gweninterpreter</groupId>
          <artifactId>gwen-web</artifactId>
          <version>${gwen.web.version}</version>
          <type>zip</type>
          <scope>provided</scope>
        </dependency>
      </dependencies>
      <build>
        <plugins>
          <plugin>
            <artifactId>maven-dependency-plugin</artifactId>
            <version>2.9</version>
            <executions>
              <execution>
                <id>install-gwen</id>
                <phase>integration-test</phase>
                <goals>
                  <goal>unpack-dependencies</goal>
                </goals>
                <configuration>
                  <includeGroupIds>org.gweninterpreter</includeGroupIds>
                  <includeArtifactIds>gwen-web</includeArtifactIds>
                  <includeTypes>zip</includeTypes>
                  <outputDirectory>target</outputDirectory>
                  <stripClassifier>true</stripClassifier>
                  <stripVersion>true</stripVersion>
                </configuration>
              </execution>
              <!--execution>
                <id>install-selenium</id>
                <phase>integration-test</phase>
                <goals>
                  <goal>copy-dependencies</goal>
                </goals>
                <configuration>
                  <excludeScope>provided</excludeScope>
                  <outputDirectory>${selenium.dir}</outputDirectory>
                </configuration>
              </execution-->
            </executions>
          </plugin>
          <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>1.3.2</version>
            <executions>
              <execution>
                <id>launch-gwen</id>
                <phase>verify</phase>
                <goals>
                  <goal>exec</goal>
                </goals>
              </execution>
            </executions>
            <configuration>
              <executable>java</executable>
              <commandlineArgs>-cp ${gwen.classpath} gwen.web.WebInterpreter -b ${gwen.args}</commandlineArgs>
              <successCodes>
                <successCode>0</successCode>
              </successCodes>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
  
</project>

Be sure to change the Gwen version by updating the gwen.web.version property at line 20 with the latest version or another version you would like to use.

With this POM and your Gwen settings in place you can have Maven download, install, and launch the latest version of Gwen with the following command (where <args> is a space separated list of arguments that you want to pass to Gwen):

mvn verify -Dgwen.args="<args>"

So if your executable tests are in a folder named ‘features’ relative to your POM file, then you can execute them with a Maven command like this:

mvn verify -Dgwen.args="features"

And to also generate html and junit style reports in the target/reports folder..

mvn verify -Dgwen.args="-r target/reports -f html,junit features"

Similarly, any arguments can be passed to Gwen in this manner through the gwen.args -D command line argument. Note that the -b batch mode switch is implicitly passed to Gwen and therefore you do not need to explicitly specify it as an argument. The gwen profile will only be activated if gwen.args is specified in the call to Maven.

By default, Gwen will use the latest version of Selenium bundled in its distribution but you can change it if you need to by performing the following. You would normally not need to do this unless you are targeting a legacy or beta browser or need to run a specific version of selenium.

  • Uncomment lines 21, 22, 27 to 31, and 61 to 71.
  • Update the selenium.version property (at line 21) with the release number you would like to use
Advertisements

No Page Objects – There’s no long way to go. We’re already there!

Specifications driven web automation with no page objects or selenium coding ~ We’ve done it!

Page Objects

I recently discovered a Page Objects Refactored article which suggests that ‘flaky’ web tests are often misattributed to Selenium web driver issues and that the problem instead is poorly coded page objects that were not written with solid OO principles in mind. Programming languages allow programmers to achieve the same or similar results in more than one way. For this reason, design patterns were introduced to help developers implement known solutions to known problems. But even design patterns require discipline and cannot be strictly enforced to any useful degree without introducing a specialised framework. Frameworks also are geared at software developers and not software users.

What is needed is a pre-programmed automation tool that frees users from development concerns.

No Page Objects

One of the primary goals of the gwen-web project is to give users a tool for automating web pages without needing to develop any page objects or compile any code. Gwen-web is an interpreter that accepts plain text Gherkin features as input and produces executing web driver instructions as output. All interactions with the web driver API happen in a precompiled and embedded web engine through a prescribed DSL. This web engine also handles many of the known gotchas and pitfalls that programmers typically face when working directly with the web driver. Users also have the flexibility to compose custom DSLs (their own step definitions) which are also expressed in Gherkin. With this approach, developing page objects and adopting design patterns is no longer required. Furthermore, programmer errors are eliminated and ‘flakiness’ is removed. The ability to compose specifications in plain Gherkin text is all that is necessary.

An example

Consider the following Gherkin feature:

 Feature: Google search

Scenario: Perform a google search
    Given I do a google search for "gwen-web"
     When I click the first result
     Then the page title should contain "gwen-interpreter/gwen-web"
      And the current URL should be "https://github.com/gwen-interpreter/gwen-web"

With Gwen, we can take a plain text feature like this one and execute it “as is”.

If you install gwen-web, you will find the above feature file located at features\google\GoogleSearch.feature relative to your root install directory (the location where you unpacked the distribution). If you open a command prompt to this location, you can execute this feature as follows:

On Windows:

gwen -b features\google\GoogleSearch.feature

On a Mac:

./gwen -b features/google/GoogleSearch.feature

When this is launched, gwen-web will:

  • Open a new browser session
  • Navigate to the google home page
  • Submit a Google search
  • Click the first result that comes back
  • Verify that the title of the resulting page contains some expected text
  • Check the URL of the resulting page

How does it work?

Alongside the GoogleSearch.feature file, you will also find a Google.meta file. This file is also a Gherkin feature file, but with a .meta file extension. It defines the step definition for the first step in our feature. The remaining steps are all predefined in the web engine. We don’t need to define step definitions for those since the web engine already knows how to execute them.

Here are the contents of the Google.meta file (minus the comments):

 Feature: Google search

@StepDef
Scenario: I do a google search for "<query>"
    Given I navigate to "http://www.google.com"
      And the search field can be located by name "q"
     When I enter "$<query>" in the search field
     Then the page title should start with "$<query>"
      And the first match can be located by class name "r"
      And the first result can be located by tag name "a" in the first match

This meta defines an annotated scenario named: I do a google search for "<query>". This is how you define a step definition with Gwen. It’s simply just a @StepDef annotated scenario that in this case accepts a <query> string as a parameter followed by a sequence of steps that define its operations. When this meta file is processed by Gwen, the step definition is loaded and made available to any executing feature that is passed in. In our example, Gwen automatically discovers this meta file (because it is in the same directory as the feature itself) and preloads the step definition defined in it to memory. When the first step in our feature executes, Gwen matches it to the step definition by name and binds the passed in “gwen-web” string literal to the <query> parameter which is then used by the steps therein to perform the Google search.

Comparing the feature step with the step definition reveals the match that Gwen is able to detect:

I do a google search for "gwen-web"
I do a google search for "<query>"

Another example

You could also express this Serenity example as a feature specification like this:

features\todo\CompleteATodo.feature

   Feature: Complete a Todo

Background: Open a new browser
      Given I start a browser for James

  Scenario: I should be able to complete a todo
      Given I browse to the application home page
       When I add a "Walk the dog" item
        And I add a "Put out the garbage" item
        And I complete the "Walk the dog" item
       Then the "Walk the dog" item should be completed

  Scenario: I should see the number of todos decrease when an item is completed
      Given I browse to the application home page
       When I add a "Walk the dog" item
        And I add a "Put out the garbage" item
        And I complete the "Put out the garbage" item
       Then the number of items left should be "1"

And make it executable with a meta specification like this:

features\todo\Todo.meta

 Feature: Todo meta

@StepDef
Scenario: I browse to the application home page
    Given I navigate to "http://todomvc.com/examples/angularjs/#/"
     Then the todo field can be located by id "new-todo"
      And the number of items left can be located by javascript "document.getElementById('todo-count').children[0]"
      And I locate the todo field

@StepDef
Scenario: I add a "<todo>" item
    Given I enter "$<todo>" in the todo field
      And the "$<todo>" item can be located by xpath "//*[@class='view' and contains(.,'$<todo>')]//label"
      And the "$<todo>" item checkbox can be located by xpath "//*[@class='view' and contains(.,'$<todo>')]//input[@type='checkbox']"

@StepDef
Scenario: I complete the "<todo>" item
    Given I click the "$<todo>" item checkbox

@StepDef
Scenario: the "<todo>" item should be completed
    Given the "$<todo>" item checkbox should be checked

And then launch it like this (assuming you have created the above two files):

Windows:

gwen -b features\todo\CompleteATodo.feature

Mac:

./gwen -b features/todo/CompleteATodo.feature

Composable automation

With Gwen we don’t have to do any heavy development work. We only need to compose specifications.

Notice that all the technical automation glue (JavaScript expressions, XPath locators etc..) are confined to meta. The features themselves are clean, readable, self documenting, and easily understood. You could easily have a BA or tester write the feature files upfront and then have a developer plug in some meta behind it. Or you could have tech savvy testers with enough HTML and JavaScript skills to write the meta themselves. Teams can choose to be as declarative or as imperative as they like in how they write their features. Tailored meta can always be crafted to suit.

So there you have it ~ web automation with #NoPageObjects and no selenium coding!

If you would like to see a more detailed example, then see our Automation by Meta blog post. See also our Wiki and FAQ pages for more information about Gwen.

Configurable User Properties in gwen-web

One of the really cool features of gwen-web is the ability to load in different parameters at run time.  This allows you to run gwen-web against multiple environments without changing either the feature or the meta files.

Lets take one of the examples with gwen-web and flood.io   Below are two examples of how one could set the value.  The first example sets “the how old are you dropdown” to 21, the second example uses a property, in this case different files for two different environments to set the value.

Lets define how the dropdown can be found in the meta first.

FloodIO.meta

And the heading should be “Step 2”
And the how old are you dropdown can be located by id “challenger_age”

FloodIO.feature

When I select “21” in the how old are you dropdown

Now with the properties approach. Lets create two files, DEV.properties and SIT.properties. The contents of the two files are below:

DEV.properties
user.age=44

SIT.properties
user.age=21

In addition we need to make a small modification to both the feature and the meta files

FloodIO.meta

And the heading should be “Step 2”
And my age is defined by property “user.age”
And the how old are you dropdown can be located by id “challenger_age”

Now replace the strikethrough line with the new line in the feature

FloodIO.feature

When I select “21” in the how old are you dropdown
When I select my age in the how old are you dropdown

Editting is complete. To run gwen-web with the environment specific age, simply type “gwen-web features/floodio/ -p dev.properties -b -r devReport”, or alternatively you can run “gwen-web features/floodio/ -p sit.properties -b -r sitReport”.

Remote WebDriver feature now available in gwen-web

For my first post, I will be talking about a new feature that has been added to gwen-web.  For those of you not familiar with gwen-web, its a web automation engine that runs inside our gwen interpreter and is used to automate the browser.  gwen-web is unique in that it allows automation of the web browser, using english(dsl) rather than coding or using visual record/playback. We believe this will remove some of the entry level requirements often associated with a coded tool.  For more information please head on over to our gwen github page https://github.com/gwen-interpreter/gwen.

Remote WebDriver

Just recently we added the remote webdriver capability.  This means gwen-web can now connect to selenium grid and drive not just the local browser, but also a browser that is installed on a different os, or a browser that has a different version.

Setting up Remote WebDriver with gwen-web

So how do I setup gwen-web with selenium remote webdriver?  What I am going to walk you through is setting up a selenium grid, and attaching nodes to it.  Firstly download the selenium-server-standalone-2.45.0.jar (or most recent) from http://selenium-release.storage.googleapis.com/2.45/selenium-server-standalone-2.45.0.jar.   Once this has been downloaded, you can start up selenium grid by running the following:

java -jar selenium-server-standalone-2.45.0.jar -role hub

Now lets make sure that its running by navigating to http://localhost:4444/grid/console.  You will notice that there are currently no browsers available.

PastedGraphic-1

Running selenium grid in this mode, means we need to register nodes against the grid.

Adding browsers to the current running grid

Lets start by downloading the latest version of chromedriver (specific for the OS) – from http://chromedriver.storage.googleapis.com/index.html?path=2.15/  (at the time of writing the blog chromedriver 2.15 is the latest)

Unzip chromedriver and note the path.

Now lets start up selenium-server-standalone as a node role, and register both the chrome driver and firefox driver.  Remembering firefox driver comes with the selenium-server-standalone.

java -jar selenium-server-standalone-2.45.0.jar 
-Dwebdriver.chrome.driver=chromedriver_215/chromedriver 
-role node 
-hub http://localhost:4444/grid/register 
-browser browserName=firefox,version=37.0,maxInstances=5,platform=LINUX 
-browser browserName=chrome,version=42.0,maxInstances=5,platform=LINUX

Let me explain the parts above:

  1. java -jar selenium-server-standalone-2.45.0.jar  (standard selenium startup)
  2. -Dwebdriver.chrome.driver= (path to chromedriver including the executable)
  3. -role node (run the selenium-server-standalone as a node)
  4. -hub http://localhost:4444/grid/register  (this is the location of the grid that you started back under the previous heading “Setting up  Remote WebDriver with gwen-web”
  5. -browser browserName=firefox,version=37.0,maxInstance=5,platform=LINUX  (the browser, version label and the number of maximum instances to run on this node)  Note:  firefox requires the binary to be in the PATH, alternatively it would need to be specified using -Dwebdriver.firefox.bin
  6. -browser browserName=chrome,version=42.0,maxInstances=5,platform=LINUX.  (register that this node is capable of running chrome)

Testing the grid console

So how do we now confirm the currently registered browsers on the grid.   Navigate to http://localhost:4444/grid/console and now the console should show both firefox and chrome registered and waiting for a connection.

PastedGraphic-2

When I setup a grid, I typically like to confirm whether or not the browsers do indeed start up.  To do this I navigate to the node webdriver/hub   E.g.  http://10.1.1.9:5555/wd/hub   (you can see the IP address of each node that is registered against this grid).

PastedGraphic-3

Then I create two sessions.  One for chrome and one for firefox.  If its working, both chrome and firefox will start.  Just delete the sessions once that has been confirmed.  If its not working, then you will need to go back and troubleshoot.  Typically its driver / binary path related.  If either browser pops up and then closes unexpectedly, make sure to keep both the selenium-server-standalone and the browser versions in sync.  The latest version of selenium-standalone may not work with a version of the browser that is 6 versions out of date.

Setting up gwen-web to work with selenium-grid

Once you have a working grid, gwen web only requires a small change to the gwen.properties file. (eg. edit ~/gwen.properties and add the following line.)

gwen.web.remote.url=http://localhost:4444/wd/hub

This tells the gwen-web engine that it needs to communicate with selenium grid which will effectively use the RemoteWebDriver with different browser capabilities.

Running gwen-web with selenium-grid

To run gwen-web with two different browsers it is suggested that you remove gwen.web.browser property from the gwen.properties and pass it in as a “-D” system property.  The below example uses the floodio challenge sample with gwen-web.

eg.

bin/gwen-web -Dgwen.web.browser=chrome -b features/floodio -r reports/chrome
bin/gwen-web -Dgwen.web.browser=firefox -b features/floodio -r reports/firefox

Once gwen-web is running, navigate to the selenium node and you will see something like this

Screen Shot 2015-04-30 at 9.42.28 pm

and navigate to the grid localhost:4444/grid/console and you will now see both firefox and chrome running.

Screen Shot 2015-04-30 at 9.42.47 pm

Automation By Meta

All that is needed to drive automation from specifications is at least one more specification.

In the previous post we used the REPL console in gwen-web to interactively complete the floodio challenge one step at a time. In this post we will use a lot of the same steps again but will automate the entire challenge through a feature file instead. Like before, there will be no need to compile any code. But unlike before, we will introduce some custom steps and compose step definitions for those in a meta file. Before proceeding, be sure to install gwen-web if you have not done so already and open a command prompt to your installation directory.

Writing the Feature

Features should be self documenting.

We start by writing a FloodIO.feature file containing the content shown below. For convenience, create this file in the root of your gwen-web installation. You can use any plain text editor you like. This will be the feature specification that we will use to automate the challenge.

   Feature: Complete the floodio challenge
       As a gwen user
       I want to automate the floodio challenge
       So that I can verify that it works

  Scenario: Launch the challenge
      Given I launch the floodio challenge
       Then I should be on the start page

  Scenario: Complete step 1
      Given I am on the start page
       When I click the Start button
       Then I should be on the step 2 page

  Scenario: Complete step 2
      Given I am on the step 2 page
       When I select "21" in the how old are you dropdown
        And I click the next button
       Then I should be on the step 3 page
       
  Scenario: Complete step 3
      Given I am on the step 3 page
       When I select and enter the largest order value
        And I click the next button
       Then I should be on the step 4 page
   
  Scenario: Complete step 4
      Given I am on the step 4 page
       When I click the next button
       Then I should be on the step 5 page

  Scenario: Complete step 5
      Given I am on the step 5 page
       When I enter the one time token
        And I click the next button
       Then I should be on the challenge completed page

The above specification consists of a feature declaration followed by several scenarios. The feature declaration is merely documentation. It has a name followed by a narrative. Each scenario has a name and a sequence of steps. If you look carefully you will discover that the steps in the feature are not the same set of steps we previously used to complete the challenge in the REPL. There are several reasons for this:

  1. We are writing the feature first and want it to read well.
  2. We are grouping steps into scenarios.
  3. We expect that all steps will execute one after the other in rapid succession when sourced from a feature file. In the REPL, pages had plenty of time to load between manually entering steps. But in batch mode we will not get any free page loading time between steps. We therefore have to introduce additional steps that will explicitly wait for pages to load before subsequent steps interact with those pages.

Writing the Meta

We are now ready to identify and compose the custom steps we have introduced. The easiest way to do this is to launch the feature and define a step definition for every step that fails to execute because it is undefined, and then repeat the process until there are no more undefined steps. We start using that approach now and issue the following command in the root of our gwen-web installation to launch the feature in batch mode (using the -b option). Batch mode forces the Gwen session to exit as soon as the feature exits.

Windows:

gwen -b FloodIO.feature

Mac:

./gwen -b FloodIO.feature

automation-by-meta-1

The output shows that the first step fails:

ERROR - Unsupported step: Given I launch the floodio challenge

This error reports that an unsupported step has been found and identifies which one it was. We can be sure now that this is a custom step and so immediately compose a step definition for it. To do this, create a new file called FloodIO.meta in the root of your gwen-web installation (alongside the feature file) and update it with the following content. Again, use any plain text editor you like.

   Feature: floodio meta
  
  @StepDef
  Scenario: I launch the floodio challenge
      Given I navigate to "https://challengers.flood.io/start"

Notice that we composed this new step definition by simply declaring a Scenario that:

  • Is annotated with the @StepDef tag
  • Has the name ‘I launch the floodio challenge‘. Note that the ‘Given’ keyword prefix is omitted from this name.
  • Contains one step that launches the browser and navigates to the challenge url.

The above step definition contains only a single step. This effectively makes ‘I launch the floodio challenge‘ an alias for ‘I navigate to https://challengers.flood.io/start‘. This may not seem very useful but what it does do in this instance is externalise the URL to the meta file. The URL is considered configuration data and therefore should be in the meta file and not the feature file.

Now when this meta is loaded, the bound navigation step will execute whenever ‘I launch the floodio challenge‘ is referenced by a step running in the interpreter.

The ‘When’, ‘Then’, ‘And’, or ‘But’ keywords would also work in place of the ‘Given’ keyword in the calling step. Which keyword you use is up to you, but you should consider both fluency and context when authoring your own features.

Next, we launch the feature again to confirm that the step now works. But this time we do it in interactive mode (without using the -b option) so that the browser remains open and the REPL mode starts after the feature exits.

Windows:

gwen FloodIO.feature

Mac:

./gwen FloodIO.feature

automation-by-meta

Gwen will automatically find the meta file if it is in the same directory as (or somewhere in the path of) the feature file. If you saved the meta somewhere else, then you can specify its location using the -m option.

The step we just composed now works and its output looks good. The start page has loaded in the browser and the console has reported a ‘Passed’ status for it. But now we get an undefined error on the next step:

ERROR - Failed step [at line 28]: Then I should be on the start page:
Unsupported or undefined step: Then I should be on the start page

So we proceed to define this step in our meta file as follows:

   
   Feature: floodio meta
        
  @StepDef
  Scenario: I launch the floodio challenge
      Given I navigate to "https://challengers.flood.io/start"
      
  @StepDef 
  Scenario: I should be on the start page
      Given I wait until "$('h1').text().trim() == 'Welcome to our Script Challenge'"
       Then I am on the start page
        And the heading can be located by tag name "h1"
        And the heading should be "Welcome to our Script Challenge"
        And the Start button can be located by name "commit"

As before, we defined this step definition as a Scenario and annotated it with the @StepDef tag, but this time we named it ‘I should be on the start page‘ and bound five steps to it:

  • The first step waits for the javascript predicate “$('h1').text().trim() == 'Welcome to our Script Challenge'” to return true. We consider the start page to be loaded when the heading is rendered in the browser. Simply checking the heading is sufficient in this case, but other pages on other sites could require more sophisticated checks. It’s up to you to discover what constitutes a page load and tailor a suitable predicate to match.
  • The second step puts the start page in scope
  • The third step defines the locator binding for the heading
  • The fourth step checks that the rendered heading content is what we expected. Although the predicate in the first step already does this, it is good practice to always check the heading (or some other text) on every page after it loads regardless. It just so happens in this instance that the predicate checks the same heading. But this will not always be so.
  • The fifth step defines the locator binding for the Start button

We now type exit in the previous Gwen session to close it and launch the feature again to confirm that the above works.

Windows:

gwen FloodIO.feature

Mac:

./gwen FloodIO.feature

automation-by-meta

We observe from the output that the custom step does work and that we have progressed beyond the first page and have made it to the second page of the challenge in the browser. We also observe that the following two steps executed successfully:

Given I am on the start page
 When I click the Start button

This is because these are not custom steps, but rather predefined steps in gwen-web. The embedded web engine in the interpreter knows how to execute them without us having to do anything. But we do again fail with a similar error to the one we got last time on the custom step that follows.

ERROR - Failed step [at line 33]: Then I should be on the step 2 page:
Unsupported or undefined step: Then I should be on the step 2 page

The Completed Meta

Repeating the process for the remaining custom steps (and doing some refactoring as we go to eliminate redundancies) yields the complete meta specification below. Most of the steps in this meta are borrowed from the previous post. We simply bound them here to step definitions and mapped them by name to the custom steps in our feature.

   Feature: floodio meta
  
  Scenario: Initialise
      Given the heading can be located by tag name "h2"
        And the next button can be located by class name "btn"
       
  @StepDef
  Scenario: I launch the floodio challenge
      Given I navigate to "https://challengers.flood.io/start"
  
  @StepDef 
  Scenario: I should be on the start page
      Given I wait until "$('h1').text().trim() == 'Welcome to our Script Challenge'"
       Then I am on the start page
        And the heading can be located by tag name "h1"
        And the heading should be "Welcome to our Script Challenge"
        And the Start button can be located by name "commit"

  @StepDef
  Scenario: I should be on the step 2 page
      Given I wait until "$('h2').text().trim() == 'Step 2'"
       Then I am on the step 2 page
        And the heading should be "Step 2"
        And the how old are you dropdown can be located by id "challenger_age"

 @StepDef
  Scenario: I should be on the step 3 page
      Given I wait until "$('h2').text().trim() == 'Step 3'"
       Then I am on the step 3 page
        And the heading should be "Step 3"
        And the largest order value is defined by javascript "Math.max.apply(Math, $.map($('.radio'), function(x) { return parseInt($(x).text()); }))"
        And the largest order input field can be located by id "challenger_largest_order"
        And the largest order radio button can be located by javascript "$('.radio:contains(${the largest order value}) input').get(0);"
      
  @StepDef  
  Scenario: I should be on the step 4 page
      Given I wait until "$('h2').text().trim() == 'Step 4'"
       Then I am on the step 4 page
        And the heading should be "Step 4"
        
  @StepDef
  Scenario: I should be on the step 5 page
      Given I wait until "$('h2').text().trim() == 'Step 5'"
       Then I am on the step 5 page
        And the heading should be "Step 5"
        And the one time token can be located by css selector ".token"
        And the one time token field can be located by id "challenger_one_time_token"
        
  @StepDef
  Scenario: I should be on the challenge completed page
      Given I wait until "$('h2').text().trim() == 'You're Done!'"
       Then I am on the challenge completed page
        And the heading should be "You're Done!"
     
  @StepDef
  Scenario: I select and enter the largest order value
      Given I type the largest order value in the largest order input field
        And I click the largest order radio button
  
  @StepDef
  Scenario: I enter the one time token
      Given I wait for the one time token text
        And I type the one time token in the one time token field

Conclusion

With the meta complete and all custom steps resolved, we now have a fully executable feature file for automating the floodio challenge. We did this by writing the feature specification first and composing the meta specification second to make the feature executable “as is” (without modifying it whatsoever). Launching the feature should now always result in success, unless something has changed in the application or it suddenly goes offline.

Windows:

gwen -b FloodIO.feature

Mac:

./gwen -b FloodIO.feature

automation-by-meta-3

Reports

If you want to also generate reports:

Windows:

gwen -b -r target\reports FloodIO.feature

Mac:

./gwen -b -r target/reports FloodIO.feature

A HTML report will be available at target/reports/index.html.

Tips

The approach presented here can be used to automate any web application and works consistently well across the four major browsers. The core tips for doing it right include:

  • Composing a custom step that will take you to the entry point of the application. The step definition for this should include all the necessary steps to get you there.
  • Composing custom wait steps for each page. For any given page, you must first wait for that page to load through an appropriately tailored JavaScript predicate before putting the page in scope, binding its locators, and performing actions and assertions on it.
  • Creating all conditions and asserting all expectations for completeness.

Next Time

In the next post we will look at properties and see how they can be used to bind environment and user data.

Update – 16 July 2015
See configurable user properties with gwen-web.

Page Objects Begone

Page objects are no longer necessary. We can emulate them with page scopes instead.

Page objects are a commonly used design pattern for developing web tests with Selenium web driver. But coding page objects and tests to an API is a very developer centric activity. The role of a tester is to write and execute tests and not to develop and compile code. The same can be said for developers when they put their test hats on.

Furthermore, Gherkin features that describe the expected behavior of a system are good enough to drive tests. There is no need for sophisticated code or a graphical UI that requires up front modelling and compilation to generate tests. It is much simpler to just specify the expected behavior in plain text and have an interpreter execute it.

Introducing gwen-web

gwen-web is a tool that aims to give testers this dynamic capability and do away with page objects and compilation altogether. It is powered by gwen; an interpreter that maps gherkin features to executable code. In the case of gwen-web, it maps Gherkin features to precompiled Selenium code to drive a web browser. As a user of this tool, you never have to write any Selenium code or develop any page objects at all.

Page Scopes

Gwen-web introduces the concept of page scopes. Page scopes are stacks of name-value pairs bound to namespaces in memory. Gwen uses them internally to manage all pages and web element bindings for you. You simply specify what pages and web elements you require using a prescribed DSL.

It is best to demonstrate with an example. The floodio challenge walks you through a series of web pages that prompt you to perform some tasks. In the remainder of this post, we will use the Gwen REPL (Read-Eval-Print Loop) console to perform these tasks and complete the challenge.

Install gwen-web – Distributable gwen-web binaries (zips) are available. See install instructions here.

After completing the installation, open a command prompt to your installed gwen-web directory and enter the following command:

Windows:

gwen

or Mac:

./gwen

REPL Console

The gwen REPL will start and you will see the following console displayed:

page-objects-begone-1

The REPL is case sensitive and does not accept fuzzy input. So be sure to enter all steps and commands exactly as shown in this post.

When Gwen starts up, it always implicitly creates and activates a default feature scope with no bindings in it. This is a global scope that can be used to set bindings for elements that are common and accessible across multiple pages (the entire feature).

The first thing we will do is create and activate a new page scope for the floodio start page. No need to write a page object, just type the following into the Gwen prompt and hit enter:

When I am on the start page

Gwen will create a new scope called “start page” and activate it in memory before informing you that it has successfully completed the step. If you enter env at the Gwen prompt, you will see the output of the environment context in memory and our newly created scope called “start page”.

{
  "scopes" : [ {
    "scope" : "start page",
    "atts" : [ ]
  } ]
}

All we have done here is defined a new page scope called “start page” and made it the currently active scope. Now we will store the url of the floodio start page into this scope so that Gwen can know where to navigate to. Enter the following to bind the floodio start page URL to the currently active “start page” scope:

Then the url will be "https://challengers.flood.io/start"

When it successfully completes, enter env at the prompt again to see our bound URL:

{
  "scopes" : [ {
    "scope" : "start page",
    "atts" : [ {
      "url" : "https://challengers.flood.io/start"
    } ]
  } ]
}

We have now informed Gwen what the url of the floodio start page is. We will now instruct Gwen to open a browser to that page. Enter the following, and observe!

Given I navigate to the start page

This step will start a new browser window, locate the URL for the start page, and point the browser to that location.

page-objects-begone-2

Gwen-web supports the Firefox, Chrome, Safari, and IE browsers. It uses Firefox as the default browser since it does not require you to install a native web driver on your local machine. To use a different browser, follow the instructions in this user guide.

Now we will tell Gwen how to locate the Start button that appears on the start page. To do that, we need to know how the button has been defined on the page. If you right click the Start button in the browser page and click “inspect element”, you will see that it is defined as an input element with a name attribute set to “commit”.

<input class="btn blue" name="commit" type="submit" value="Start">

Now enter the following at the Gwen prompt to bind this information to the “start page” scope that is currently active:

And the Start button can be located by name "commit"

Type env at the Gwen prompt again if you would like to see how this locator information is bound to memory.

Before continuing, be sure to close the “inspect element” pane if you still have it open in your browser. Leaving it open may interfere with Gwen and could result in errors.

Now enter the following to verify that Gwen can locate the Start button:

And I locate the Start button

This step locates the Start button and highlights it for a short time. You should see the Start button being highlighted and then unhighlighted.

page-objects-begone-3

This confirms that Gwen can locate the button and that we have bound the locator correctly. Now enter the following to have Gwen click the Start button:

When I click the Start button

Gwen will now click the Start button and the browser will navigate to the next page.

page-objects-begone-4

To proceed from here, we again first need to create a new page scope for this page, and then tell Gwen how to locate the elements we wish to interact with. Enter the following to create a new page scope for this page:

Then I am on the step 2 page

This will create a new empty scope in memory called “step 2 page” and make it the currently active scope. If you type env at the command prompt, you will see it printed at the bottom.

{
  "scopes" : [ {
    "scope" : "step 2 page",
    "atts" : [ ]
  } ]
}

By default, the env command displays only the currently visible scopes (in this case, the step 2 scope). To display all scopes (including our previously created start page scope), you need to specify the -a switch.

To see all scopes, type env -a:

{
  "scopes" : [ {
    "scope" : "start page",
    "atts" : [ {
      "url" : "https://challengers.flood.io/start"
    }, {
      "the Start button/locator" : "name"
    }, {
      "the Start button/locator/name" : "commit"
    }, {
      "the Start button/click" : "true"
    } ]
  }, {
    "scope" : "step 2 page",
    "atts" : [ ]
  } ]
}

All page scopes are managed in memory as JSON objects on a stack with the most recently active scope appearing at the bottom. For more details about how this stack works, you can study the documented source on the Gwen project site here.

Moving on, we can now proceed to bind the locator information for the ‘how old are you’ dropdown and the ‘Next’ button elements that appear on the step 2 page. If you inspect these elements in the page source, you will find that they are defined as follows:

<select class="select optional" id="challenger_age" name="challenger[age]">
    <option value="">How old are you?</option>
    <option value="18">18</option>
    <option value="19">19</option>
    ...
</select>
<input class="btn" name="commit" type="submit" value="Next">

Again, be sure to close the “inspect element” pane (if you opened it) in the browser before continuing.

Enter the following into the gwen prompt to let Gwen know how to locate the ‘how old are you’ dropdown element (in this instance we locate it by Id).

Given the how old are you dropdown can be located by id "challenger_age"

Now for the next button. We could locate it by name in the same way we did for the Start button on the start page, but to mix things up a bit (and because we can), we will locate it by class instead. Enter the following to let Gwen know how to locate the next button:

And the next button can be located by class name "btn"

You can confirm that both of these locators work by entering the following steps in the console (one after the other) to highlight them:

And I locate the how old are you dropdown
And I locate the next button

After confirming the above, we can proceed to select an age and click the next button before creating a scope for the next page:

 And I select "21" in the how old are you dropdown
 And I click the next button
Then I am on the step 3 page

We are now on the step 3 page. Here we need to select and enter the largest order value from the list of radio buttons displayed on the page.

page-objects-begone-5

How do we instruct Gwen to find the largest order value? Lets see if we can find it with some JQuery scripting. Open the browser console view and enter the following lines of script to locate the largest order value:

var values = $.map($('.radio'), function(x) { return parseInt($(x).text()); });
var maxValue = Math.max.apply(Math, values);

Now that we have the largest order value, we can locate the associated radio button as follows:

var maxRadio = $('.radio:contains("' + maxValue + '") input[type="radio"]');

We have shown above that we can locate the largest order value using JQuery. Luckily for us, gwen-web allows us to locate elements by JavaScript (but only through one liner expressions). Furthermore, if the page in the browser has loaded the JQuery library then we can use it too. Now enter the following steps into the console REPL so that Gwen can find the largest order value and locate its associated radio button:

Given the largest order value is defined by javascript "Math.max.apply(Math, $.map($('.radio'), function(x) { return parseInt($(x).text()); }))"
  And the largest order radio button can be located by javascript "$('.radio:contains(${the largest order value}) input[type="radio"]').get(0);"

String Interpolation:

Notice how we referenced ${the largest order value} defined by the first step inside the JavaScript locator expression of the second step. This saves us from repeating the expression used to find the largest order value across two steps.

Now enter the following to locate the radio button having the largest order value to confirm that it works:

And I locate the largest order radio button

We had to use some JQuery scripting here to locate the largest order value. Scripting is necessary in this case because we need to enter values and select buttons based on the results of a function applied to the elements and values on this page. This is a good example of an interaction that cannot be readily automated without some scripting.

The other things we need to locate on this page are the input field into which we will need to enter the largest value into, and the next button. We define the locators for these by entering the following:

And the largest order input field can be located by id "challenger_largest_order"
And the next button can be located by class name "btn"

We are now ready to let Gwen find the largest order value, type it into the input field, click the radio button for that value, and then click next. We do that by entering the following steps:

Given I type the largest order value in the largest order input field
 When I click the largest order radio button
  And I click the next button
 Then I am on the step 4 page

The step 4 page merely asks us to click the next button when we are ready. We do this straight away by entering the following:

Given the next button can be located by class name "btn"
 When I click the next button
 Then I am on the step 5 page

This will bring us to the step 5 page (the last page in the floodio challenge).

page-objects-begone-6

This page requires that we copy a provided token value into a field and then click next. Inspecting the page source reveals that the token is displayed in a span element defined with a class="token" attribute. The input field has an id of “challenger_one_time_token”, and the next button is defined in the same way it was in the previous pages. We now define locators for these by entering:

Given the one time token can be located by css selector ".token"
  And the one time token field can be located by id "challenger_one_time_token"
  And the next button can be located by class name "btn"

If you take a closer look at the source you will see that the token value is actually loaded by an ajax request when the page is loaded. Waiting for this value to load is not a problem when running Gwen in REPL mode as the ajax request will have most likely completed in the time that the REPL is idly waiting for us to enter the next step at the prompt. But if we were running all the steps we’ve entered so far very quickly (as in automated batch mode), then we would need to wait for the ajax request to complete and the token value to be populated first before attempting to access the value. Enter the following to have Gwen wait for the token value to be populated (note: if the token is already populated, then Gwen will immediately return without waiting).

And I wait for the one time token text

Now that we know the token is loaded, we can proceed to copy it into the input field and click the next button to complete the challenge. Enter the following steps to do that:

Given I type the one time token in the one time token field
 When I click the next button
 Then I am on the done page

This will take us to the done page, telling us that we’re done.

page-objects-begone-7

To finish, type exit at the Gwen prompt. The browser and the REPL will shut down.

Separation of Concerns

Two major benefits that page objects provide include the separation of test logic from configuration and the elimination of unwanted redundancies. Gwen can provide these same benefits through meta features.

Configuration by Meta

A meta feature is simply a configuration specification expressed as a gherkin feature (a feature describing a feature if you like). Recall that some of the steps we typed into the REPL above just configure (or tell Gwen) what the url to a page is or how an element on a page can be located. This configuration information can all be captured in a meta feature file and loaded into Gwen on startup. We can also eliminate duplicated steps and other redundancies too. For example, you will have noticed that the step that configures the next button locator was repeated above for every page that contains a next button. Since this button is common across multiple pages, it makes sense to define it once and reuse it. This can be done by binding it to the global feature scope that is implicitly created and activated by Gwen when it starts up.

So we can now capture all the configuration steps into a single meta feature file. Note that this meta is representative of everything that would otherwise need to be programmed into a page object. But with Gwen meta features, that programming is never necessary.

Now create a new file called floodio.meta in your gwen-web install directory and edit it to contain the following meta specification:

 Feature: Flood IO Meta

Scenario: Configure common locators
    Given the next button can be located by class name "btn"

Scenario: Configure start page
     When I am on the start page
     Then the url will be "https://challengers.flood.io/start"
      And the Start button can be located by name "commit"

Scenario: Configure step 2 page
     When I am on the step 2 page
     Then the how old are you dropdown can be located by id "challenger_age"

Scenario: Configure step 3 page
     When I am on the step 3 page
     Then the largest order value function is "Math.max.apply(Math, $.map($('.radio'), function(x) { return parseInt($(x).text()); }))"
     Then the largest order value is defined by javascript "${the largest order value function}"
      And the largest order radio button can be located by javascript "$('.radio:contains("' + ${the largest order value function} + '") input[type="radio"]').get(0);"
      And the largest order input field can be located by id "challenger_largest_order"

Scenario: Configure step 4 page
   # noop - this page only has a next button (we have already defined a common locator for it above)
    
Scenario: Configure step 5 page
     When I am on the step 5 page
     Then the one time token can be located by javascript "$('.token').get(0)"
      And the one time token field can be located by id "challenger_one_time_token"

Loading Meta

The above meta can now be loaded into Gwen on startup through the -m command line option as follows:

Windows:

gwen -m floodio.meta

Mac:

./gwen -m floodio.meta

You will notice this time that the REPL console will load the meta when it starts up.

page-objects-begone-8

You can type env -a at the Gwen prompt to view all the meta bindings in memory:

{
  "scopes" : [ {
    "scope" : "feature",
    "atts" : [ {
      "the next button/locator" : "class name"
    }, {
      "the next button/locator/class name" : "btn"
    }, {
      "the largest order value function" : "Math.max.apply(Math, $.map($('.radio'), function(x) { return parseInt($(x).text()); }))"
    } ]
  }, {
    "scope" : "start page",
    "atts" : [ {
      "url" : "https://challengers.flood.io/start"
    }, {
      "the Start button/locator" : "name"
    }, {
      "the Start button/locator/name" : "commit"
    } ]
  }, {
    "scope" : "step 2 page",
    "atts" : [ {
      "the how old are you dropdown/locator" : "id"
    }, {
      "the how old are you dropdown/locator/id" : "challenger_age"
    } ]
  }, {
    "scope" : "step 3 page",
    "atts" : [ {
      "the largest order value/javascript" : "Math.max.apply(Math, $.map($('.radio'), function(x) { return parseInt($(x).text()); }))"
    }, {
      "the largest order radio button/locator" : "javascript"
    }, {
      "the largest order radio button/locator/javascript" : "$('.radio:contains(\"' + Math.max.apply(Math, $.map($('.radio'), function(x) { return parseInt($(x).text()); })) + '\") input[type=\"radio\"]').get(0);"
    }, {
      "the largest order input field/locator" : "id"
    }, {
      "the largest order input field/locator/id" : "challenger_largest_order"
    } ]
  }, {
    "scope" : "step 5 page",
    "atts" : [ {
      "the one time token/locator" : "javascript"
    }, {
      "the one time token/locator/javascript" : "$('.token').get(0)"
    }, {
      "the one time token field/locator" : "id"
    }, {
      "the one time token field/locator/id" : "challenger_one_time_token"
    } ]
  } ]
}

Now we can open a new browser session to the floodio start page and click the start button by entering only the following steps into the REPL:

Given I navigate to the start page
 When I click the Start button
 Then I am on the step 2 page

And similarly for the remaining pages..

Step 2 page:

Given I select "21" in the how old are you dropdown
 When I click the next button
 Then I am on the step 3 page

Step 3 page:

Given I type the largest order value in the largest order input field
  And I click the largest order value
 When I click the next button
 Then I am on the step 4 page

Step 4 page:

When I click the next button
Then I am on the step 5 page

Step 5 page:

Given I wait for the one time token text
  And I type the one time token in the one time token field
 When I click the next button
 Then I am on the done page

When ready, type exit to close the browser and quit the REPL.

Conclusion

In this post, we used the Gwen REPL to directly interact with a web application from scratch. We then factored out the configuration steps into a meta feature and loaded that into the REPL and interacted with the same web application again without reentering any of the configuration steps. We did it all with no page objects too! 🙂

Next..

In the next post we will write a feature file to automate the same floodio challenge in batch mode and generate evaluation reports. We will also compose custom step definitions and perform some assertions on each page. Lastly we will solve some common ajax and page loading problems that arise when automating web tests.

Update 12 Jan 2015 – As promised, here is the follow up post.