I write this blog with an intention to share ideas and to provide practical approaches on building test automation framework, using CucumberJVM and Guice, a dependency injection framework from Google. I estimate that it may take roughly between 3 hours and 4 hours in setting this up from a scratch, for a person who has a bit of programming experience.
Scenarios To Be Considered
Let’s consider the following two scenarios as an example, mainly to understand the further steps ahead in the blog
- Create a new account
- Search products and place an order
Requirements
Step 0—Define scenarios to be automated in BDD format
Let’s begin by writing steps for above two scenarios in Gherkin format. We need them as we use these to learn the concepts required in building a robust and scalable automation framework. For instance, the following steps are to be created for the first scenario: Create a new account
in a feature file
@Smoke @NewAccount
Feature: Account Creation related scenarios
Scenario Outline: Create a new account
Given User opens home page: url
When User navigates to create new customer account
And User enters auto-generated personal info
And User checks the checkbox 'Sign Up for Newsletter'
And User enters following sign-in info:
| email | |
| password | |
| confirmPassword | |
Then User creates an account and verifies the message: "<message>"
Examples:
| message |
| Thank you for registering with Main Website Store. |
All the features being used in this project can be viewed from this folder: features For further reference on as to how to write scenarios on Gherkin, you can follow my blog post on Gherkin
Step 1—Create a custom module and an injector source
The next step is to create a class that extends Guice’s AbstractModule, where in we define methods that provide all necessary objects while injecting in step definition classes, and also page classes.
import com.google.inject.AbstractModule;
public final class FrameworkModule extends AbstractModule {
}
In CucumberInjectorSource
class, there are two modules that are created via injectors — the first one is Cucumber related which comes from cucumber-guice
library. This mainly ensures that all the page objects created are all on scenario scope, and are destroyed once the scenario ends.
In addition to Cucumber module, we also pass our own module FrameworkModule
that we defined in the previous step to the injector source that takes care of creating all dependent objects and injecting them where needed, mostly on step definition and page classes.
public class CucumberInjectorSource implements InjectorSource {
@Override
public Injector getInjector() {
return Guice.createInjector(Stage.PRODUCTION, CucumberModules.createScenarioModule(),
new FrameworkModule());
}
}
Step 2—Create a method that provides test configurations
Now that module is defined , we’ll proceed by adding a provider within a module that create and returns an object to hold all test configuration like application url, browser to be used etc. This is a simple POJO class, created using the values from a file in yaml format. Note that scope here, which is @Singleton
, meaning we will use the same singleton object throughout a test run.
@Provides
@Singleton
TestConfig providesTestConfiguration() {
Yaml yaml = new Yaml();
InputStream inputStream = this.getClass().getClassLoader()
.getResourceAsStream("config/test-config.yml");
TestConfig testConfig = yaml.loadAs(inputStream, TestConfig.class);
return testConfig;
}
Step 3—Create a method that provides TestContext
In this step, we are just adding another provider TestContext
which is plainly to share states between steps within the same class and between the step definition classes. It stores values using key and value. Also note that scope is @ScenarioScoped
which will ensure that TestContext
object is be recreated for every scenario.
@Provides
@ScenarioScoped
TestContext<String, Object> providesTextContext() {
return new TestContext<>();
}
Step 4—Create a provider that returns a standard Faker object
In this, one of the providers in FrameworkModule
returns Faker
object which will be injected into the class, preferably in the step definition classes where we need randomly generated data, which will be used in our tests. Faker object will be in @Singleton
scope.
@Provides
@Singleton
Faker providesFakerInstance() {
return new Faker();
}
Step 5—Create a provider that returns a WebDriver
This is a provider that returns WebDriver
instance, for a given browser type. It's also worth to note that we also inject TestConfig
object in the parameter, created in the previous step that takes care of providing the value for browser type. In this case, it is designated as @ScenarioScoped
as I intend to use a new browser instance for each scenario in the suite.
@Provides
@ScenarioScoped
@Inject
WebDriver providesBrowserInstance(TestConfig testConfig) {
String browser = Optional.ofNullable(testConfig.getBrowser()).get();
switch (browser) {
case "firefox":
return WebDriverManager.firefoxdriver().create();
case "edge":
return WebDriverManager.edgedriver().create();
default:
return WebDriverManager.chromedriver().create();
}
}
Putting all these providers together, this is the view of FrameworkModule
class: FrameworkModule
Step 6—Create pages with required elements and methods:
There are several page classes that we may have to create in reality, however for the sake of simplicity, let’s consider this class ReviewAndPaymentsPage
that is implemented for Review & Payments page in our test app. If you notice that this class is scenario scoped, as mentioned previously, here we are instantiating a page object, keeping it throughout the scenario, and discarding it once the scenario is done, irrespective of the test status.
The next thing to note here is that, we are injecting WebDriver
object in the page constructor parameter which is required to initialize the page elements using PageFactory design.
Lastly, we declared a page element placeOrderBtn
and implemented a method placeOrder
which is to click and place an order.
The same pattern can be followed while implementing other pages on the testing app.
@ScenarioScoped
public class ReviewAndPaymentsPage extends BasePage {@Inject
public ReviewAndPaymentsPage(WebDriver driver) {
super(driver);
PageFactory.initElements(driver, this);
}
@FindBy(how = How.CSS, using = "button[title='Place Order']")
private WebElement placeOrderBtn;
public void placeOrder() {
waitUntil(placeOrderBtn);
pause(Duration.ofSeconds(3));
placeOrderBtn.click();
}
}
For the testing app, I used for this workshop, all the pages are defined within this package here:pages
Step 7—Define glue codes for the steps in feature files:
This is simple step definition class that contains an implementation for a step User reviews payments and places order
. Since it's required ReviewAndPaymentsPage
object to place an order, we inject it using Guice
through the module we defined on the first step. This way, we are allowed to inject other objects like Faker, TestContext, TestConfig etc. in similar way.
public class ReviewAndPaymentsSteps {@Inject
ReviewAndPaymentsPage reviewAndPaymentsPage;
@Then("User reviews payments and places order")
public void userReviewsPaymentsAndPlacesOrder() {
reviewAndPaymentsPage.placeOrder();
}
}
Likewise, all the glue codes are defined within this package here: glue and can also find complete sources on this repository.
Godspeed!
Veera.