Creating your own steps - veepee-oss/gingerspec GitHub Wiki

Developers, QAs, and business people may use different terminology or "language" to talk about the problem domain. BDD, when applied correctly, looks to smooth out this with the creation of a ubiquitous domain language (DSL) that correctly describes the domain problem and that can be understood by people at all levels. To achieve this, this DSL should be focused on behavior, that is the "what" instead of the "how" (what the system should do, not how it does it) and should not talk about the specifics of the implementation. GingerSpec pre-made steps were designed to speed up automation, kind of a VERY general DSL for the creation of integration tests, but they may not properly model a more specific domain problem.

All steps included in Gingerspec were created with the idea of reusability and flexibility. The problem is, that in order to achieve this, the steps must be low level enough to be used for a wide range of cases. You can create your own steps with the level of abstraction that you prefer, however, I would like you to consider the following points before trying to apply BDD with GingerSpec or even using Cucumber in general:

  • The main goal of GingerSpec is to simplify and speed up the development of integration tests by providing basic building blocks in Gherkin. Since there is no need of creating Java step definitions or "glue" code, you will only have to maintain one layer of code, and that is the Feature file. When creating your own steps, you will have to maintain two layers of code: the Feature file and the Java step definitions. This makes it great for newcomers and people with little experience with Java.

  • GingerSpec was designed to work as general DSL for the creation of integration tests. There are several reasons to keep the Given-When-Then structure of BDD, like making the tests more readable, and the fact Gherkin is supported by many IDEs.

  • GingerSpec is, in itself, a super-simplified programming language that heavily borrows BDD syntax.

  • Cucumber, when used outside of its intended scope (collaboration at the beginning of the development process), becomes in no more than a simple wrapper between integration tests. Try to consider: are you really expecting your Product owner or business people to continue using your Gherkin tests as documentation? are you expecting them to modify and run the tests?

  • For certain types of applications (like human-facing applications such as web pages), the creation of new and more abstract steps with higher functionality makes sense, since what is under test is a behavior. In that case, you can create your own steps and just call GingerSpec methods behind the scenes using the GingerSpec API. But in other applications like REST API testing, where what is under test is a contract and not a behavior, this makes little sense. In these cases, the system is much better described using a very low-level language.

Creating your own steps

Creating your own step logic

You can create your own step definitions in Java code and then link these step definitions to specific Gherkin steps. There is already a very good tutorial in the official cucumber documentation about how to do this.

This process can be further simplified if you're using an IDE that has support for Gherkin syntax. If you have installed and configured Intellij IDEA, you can do this directly in the Feature file

creating a gherkin step definition

Check the official documentation of Intellij IDEA here where this process is explained in detail.

Selenium example

When you are creating your own steps for UI testing, you can get the instance of the current selenium Driver GingerSpec is using (GingerSpec already contains Selenium, so you don't need to add the selenium dependency in your POM if you already have the GingerSpec dependency). If you added the @web annotation in your Gherkin Feature or Scenario, GingerSpec will automatically create a new Selenium driver for you in an internal @Before cucumber hook, and you can have access to this driver instance via the getDriver() method in the CommonG class.. Check the following example to see how to do this

import com.privalia.qa.specs.BaseGSpec;
import com.privalia.qa.specs.CommonG;
import io.cucumber.java.en.Given;
import org.openqa.selenium.WebDriver;

public class StepDefinitions extends BaseGSpec {

    public StepDefinitions(CommonG spec) {
        this.commonspec = spec;
    }

    @Given("I open the page {string}")
    public void openUrl(String url) {
        WebDriver d = this.commonspec.getDriver();
        d.get(url);
    }
}

If you don't know what a cucumber hook is, or you want to create your own, you can check the official cucumber documentation.

If you created your project using the instructions provided here, the resulting project will contain pre-made examples of this. You can even mix your own selenium code with the methods contained in GingerSpec. For this, just create a new instance of the SeleniumGSpec class to have access to all Selenium-related functionality of GingerSpec as shown in the following example:

import com.privalia.qa.specs.BaseGSpec;
import com.privalia.qa.specs.CommonG;
import com.privalia.qa.specs.SeleniumGSpec;
import io.cucumber.java.en.Given;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;

public class StepDefinitions extends BaseGSpec {

    SeleniumGSpec seleniumGSpec;

    public StepDefinitions(CommonG spec) {
        this.commonspec = spec;

        /* Access all functions for working with selenium */
        seleniumGSpec = new SeleniumGSpec(this.commonspec);
    }

    @Given("I open the page {string}")
    public void openUrl(String url) {
        WebDriver d = this.commonspec.getDriver();
        d.get(url);

        /* Using GingerSpec methods */
        this.seleniumGSpec.seleniumTypeByLocator("Rick and Morty", "name", "q", 0);

        /* Using plain old Selenium */
        d.findElement(By.name("q")).sendKeys("Rick and Morty");

    }
}

Rest API example

GingerSpec also works with REST-assured, so, you DONT need to add the REST-assured dependency to your POM file in Maven if you already have the GingerSpec dependency. The steps for testing rest services that are included with GingerSpec make use of the RequestSpecification object of RestAssured to build each request, so if you want to get the instance of this object to use it in your own steps, you can do something like the following:

@Given("lotto results return the expected response")
    public void lotto_resource_returns_200_with_expected_id_and_winners(String url) {
        RequestSpecification d = this.commonspec.getRestRequest();

        when().
                get("/lotto/{id}", 5).
                then().
                statusCode(200).
                body("lotto.lottoId", equalTo(5),
                        "lotto.winners.winnerId", hasItems(23, 54));
    }

The @rest hook tag is responsible for creating a new RequestSpecification object when each scenario starts.

Or you also can just use RestAssured directly like this (you are not gonna mix GingerSpec steps and your steps):

    @Given("lotto results return the expected response")
    public void lotto_resource_returns_200_with_expected_id_and_winners(String url) {
        RestAssured.when().
                get("/lotto/{id}", 5).
                then().
                statusCode(200);
    }

Other technologies

GingerSpec has separated classes that contain the step definitions for each technology domain (Selenium, REST, Databases, Kafka, etc). You can get a list of all the step definitions of each one by directly checking the API. If you want to use any of this functionality, just create a new instance of the corresponding class in the same way as described before for Selenium:

    SeleniumGSpec seleniumGSpec;
    RestSpec restSpec;
    SqlDatabaseGSpec sqlDatabaseGSpec;
    BigDataGSpec bigDataGSpec;
    FileParserGSpec fileParserGSpec;
    KafkaGSpec kafkaGSpec;
    SoapServiceGSpec soapServiceGSpec;
    SshGSpec sshGSpec;
    UtilsGSpec utilsGSpec;

    /**
     * Example of how to inherit the needed objects from gingerspec
     */
    public CustomStepsDefinition(CommonG spec) {

        this.commonspec = spec;

        /* Access all functions for working with selenium */
        seleniumGSpec = new SeleniumGSpec(this.commonspec);

        /* Access all functions for working with REST services */
        restSpec = new RestSpec(this.commonspec);

        /* Access all functions for working with relational databases */
        sqlDatabaseGSpec = new SqlDatabaseGSpec(this.commonspec);

        /* Access all functions for working with Big data functionality */
        bigDataGSpec = new BigDataGSpec(this.commonspec);

        /* Access all functions for handling and parsing text files */
        fileParserGSpec = new FileParserGSpec(this.commonspec);

        /* Access all functions for working with kafka */
        kafkaGSpec = new KafkaGSpec(this.commonspec);

        /* Access all functions for working with SOAP web services */
        soapServiceGSpec = new SoapServiceGSpec(this.commonspec);

        /* Access all functions for running bash commands and establishing SSH connections */
        sshGSpec = new SshGSpec(this.commonspec);

        /* Access all other useful functions/operations */
        utilsGSpec = new UtilsGSpec(this.commonspec);

    }

Using assertions

GingerSpec comes preloaded with AssertJ, a very popular assertions java library. So, whenever you create your own steps, it is recommended to use AssertJ to make assertions in the Java code, for example:

// basic assertions
Assertions.assertThat(frodo.getName()).isEqualTo("Frodo");
Assertions.assertThat(frodo).isNotEqualTo(sauron);

// chaining string specific assertions
Assertions.assertThat(frodo.getName()).startsWith("Fro")
                           .endsWith("do")
                           .isEqualToIgnoringCase("frodo");

// as() is used to describe the test and will be shown before the error message
Assertions.assertThat(frodo.getAge()).as("check %s's age", frodo.getName()).isEqualTo(33);

Check the official AssertJ documentation for more examples


Merging low-level steps

This could certainly be the easiest way in the sense that you can just reuse the steps already contained in Gingerspec and group several of them to create a single and more high-level step

Consider the following scenario as an example. the scenario contains 4 low-level GingerSpec steps. It can be converted from this:

    Scenario: Verify that there are two main sections "Interactions" and "Widgets"
        Given My app is running in 'demoqa.com:80'
        And I browse to '/'
        When '2' elements exists with 'class:widget-title'
        And I wait '1' seconds

To this:

    Scenario: This is the same scenario as above, but in one line, using custom steps
        Given I verify the Interactions and Widgets sections are present

To accomplish this, you will have to first create the new step definition and just call the underlying java functions for the low-level steps with the correct parameters, like this:

    @Given("^I verify the Interactions and Widgets sections are present$")
    public void iVerifyTheInteractionsAndWidgetsSectionsArePresent() throws Throwable {
        seleniumGSpec.setupApp("demoqa.com:80");
        seleniumGSpec.seleniumBrowse(null,"/");
        seleniumGSpec.assertSeleniumNElementExists("at least", 2,"class","widget-title");
        utilsGSpec.idleWait(1);
    }

Remember that the java class where you create your new steps definitions must be included in the glue parameter in the runner class.

Since hunting down the underlying functions and parameters of all steps in a scenario can be a long and painstaking process, GingerSpec provides a feature that makes this process much easier. Run your tests with the -DSHOW_STACK_INFO and you will get information about the underlying java function and parameters that are being called for each gherkin step

Example:

mvn verify -Dit.test=eu.vptech.firstProject.tribe1.CucumberSeleniumIT -DSHOW_STACK_INFO

Output:

  Scenario: Verify that there are two main sections "Interactions" and "Widgets" # src/test/resources/features/tribe1/cucumber_selenium_test.feature:19
    Given My app is running in 'demoqa.com:80'
         # SeleniumGSpec.setupApp(String)
         # Argument 0: demoqa.com:80
    And I browse to '/'
         # SeleniumGSpec.seleniumBrowse(String,String)
         # Argument 0: null
         # Argument 1: /
    When '2' elements exists with 'class:widget-title'
         # SeleniumGSpec.assertSeleniumNElementExists(String,Integer,String,String)
         # Argument 0: null
         # Argument 1: 2
         # Argument 2: class
         # Argument 3: widget-title
    And I wait '1' seconds
         # UtilsGSpec.idleWait(Integer)
         # Argument 0: 1

Where:

Given My app is running in 'demoqa.com:80' -> Step being executed
         # SeleniumGSpec.setupApp(String)  -> Underlying Java method being executed and the type of each parameter 
         # Argument 0: demoqa.com:80       -> Value of first parameter

-DSHOW_STACK_INFO is only available in Gingerspec 2.0.3 and up