Scaffold is a Selenium WebDriver abstraction built in Java 11 with Spring Boot 2.x / Jersey / Jax Rs. Out of the box, it provides a myriad of additional features on top of the base WebDriver wrapper:
To view the most current version, visit the Central Repository.
The build can be found on Travis CI.
Use this section for setting up a new project using Scaffold. Setup follows a fairly standard Spring Boot application design by using modules for the code base's environment and main testing.
Coming in a future update, we will provide an example implementation project and a maven archetype to easily start up new projects.
Create a new Java Maven project.
Follow the standard maven naming conventions when creating the new project.
Add the following dependency to your parent POM's DependencyManagement section:
<dependency>
<groupId>com.retailmenot.scaffold</groupId>
<artifactId>scaffold</artifactId>
<version>current_version</version>
</dependency>
<dependency>
<groupId>com.retailmenot.scaffold</groupId>
<artifactId>framework</artifactId>
<version>current_version</version>
</dependency>
<dependency>
<groupId>com.retailmenot.scaffold</groupId>
<artifactId>environment</artifactId>
<version>current_version</version>
</dependency>
Create two new maven modules in your project.
environment
core
The environment module's POM should contain Scaffold's environment dependency.
<dependency>
<groupId>com.retailmenot.scaffold</groupId>
<artifactId>environment</artifactId>
</dependency>
The core module's POM should contain Scaffold's framework dependency.
<dependency>
<groupId>com.retailmenot.scaffold</groupId>
<artifactId>framework</artifactId>
</dependency>
The following are files that should be created in your project.
A spring configuration file is necessary for handling component scanning and the usage of spring profiles when running testing. Component scanning is necessary for dependency injection and spring profiles are used for setting
DesiredCapabilities
for testing. More detail regarding spring profiles are included in a later section.
Create a new file under the environment module adhering to the naming convention standardization hierarchy set when creating your new project.
E.G: environment > src > main > java > your > groupID > environment > YourProjectConfiguration.java
The contents of this file should include:
@Configuration
@ComponentScan(value = "your.group.id")
@PropertySource("classpath:/application.properties")
public class YourProjectConfig {
}
Scaffold implements the paradigm of a page object. Page objects are simple representations of web pages as a Java Object. In order to create a representation of a web page, you'll need to create a new class that contains properties of Scaffold's strongly typed elements.
Scaffold makes available particular types of WebElements for you to use, which narrow the scope of methods available for any given WebElement, and keep the user focused on the actions they should be performing on these elements.
For example, a user cannot sendKeys()
to a Button or a Link. The ButtonWebElement
and LinkWebElement
objects take that into account and don't expose those methods.
Another advantage of these elements is that they manage all interaction with the WebDriver internally. Most frameworks require the test write/page object maintainer to use the WebDriver to perform all their actions, which requires a lot of Selenium knowledge, and can also lead to race conditions, thread-safety issues, and the exposure of unnecessary complexity to the testers.
In the millions of test cases run by Scaffold over the years, there has never been a reported occurrence of a StaleElementReferenceException. This is possible because the framework manages the WebDriver and the WebElements internally, in a thread-safe and careful manner.
A strongly typed element in scaffold can be one of the following:
Page objects should only be written in a way that makes them agnostic to navigation, the web driver itself, or by any other external means outside the scope of its own representation. They should also never contain any
assertions. While the WebDriver
is not directly injected into a page object, the underlying WebDriver
is used for finding elements when a getElement()
is performed and not when an element is declared.
In other words, simply initializing a strongly typed element does not perform an interaction with the underlying WebDriver; but instead, only creates a reference point with a By locator. This creates a dynamic use case when
performing navigation in that when a new page object is initialized, the elements will not be searched for at the time of the class being constructed. It isn't until you attempt to get the strongly typed element with a getter
that the underlying WebDriver will perform a getWebElement()
, therefore performing the findElement()
interaction.
To create a page object, follow the same design as Java Beans, found here.
The page objects should live within the core module in a page module. E.G: core > src > main > java > your > groupID > page
An example page object:
public class LoginPage {
private DivWebElement pageHeader = new DivWebElement(By.cssSelector("#someHeader"));
private InputWebElement emailInput = new InputWebElement(By.cssSelector("#emailInput"));
private InputWebElement passwordInput = new InputWebElement(By.cssSelector("#passwordInput"));
private ButtonWebElement loginButton = new ButtonWebElement(By.cssSelector("#loginButton"));
public DivWebElement getPageHeader() {
return pageHeader;
}
public DivWebElement getEmailInput() {
return profileGreeting;
}
public LinkWebElement getPasswordInput() {
return editProfileLink;
}
public DivWebElement getLoginButton() {
return emailList;
}
public void clickLoginButton() {
getLoginButton().click();
}
public void login(String username, String password) {
getEmailInput().clearAndSendKeys(username);
getPasswordInput().clearAndSendKeys(password);
getLogInButton().click();
}
}
Let's break down what we see in the example above.
By
class.clickLoginButton()
method in a test, the AbstractWebElement
class will perform a find elementWebDriverWrapper
to find the element loginButton
, and then perform the click action from AbstractClickable
.Your project should have a navigation file that extends WebDriverNavigation
. This file should live within the core module in a navigation package. E.G: core > src > main > java > your > groupID > page > Navigation.class
The navigation file handles the navigation of the page objects. For example, using the example login page object above, let's say your implementing project has a login page and a profile page.
In order to navigate to the profile page, it first requires a login (since the profile is gated). Your page navigation file might contain the method navigateToProfilePage
:
@Component
public class AutomationNavigation extends WebDriverNavigation {
// Spring environment variable from a configuration file in environment module
private final String baseEnvironmentUrl;
public NavigationImpl(@Value("${base-environment-url}") String baseEnvironmentUrl) {
this.baseEnvironmentUrl = baseEnvironmentUrl;
}
public ProfilePage navigateToProfilePage(String username, String password) {
// Start the web test from a base environment URL pulled in from a Spring env variable
getWebDriverWrapper().get(baseEnvironmentUrl);
// Create a new instance of the LoginPage
var logInPage = new LoginPage();
// Use the login method from the LoginPage
logInPage.login(username, password);
// Return the new ProfilePage
return new ProfilePage();
}
}
Let's break down what we see in this above example.
@Component
WebDriverNavigation
is important here so we can get the WebDriverWrapper
instance from the current thread. That allows us to perform the .get
call.@Value("${base-environment-url}") String baseEnvironmentUrl
. This is a spring environment variable defined in a spring profile (more on that later). This allows us have a configure whatget()
function to navigate to our base environment url set by a spring profile.The advantage here for separating out the navigation is that it gives us further abstraction in our test writing. Or, in other words, creating an additional layer that will allow us to maintain our testing a little easier as it scales.
Your project should have a BaseTest file that extends ScaffoldBaseTest
. This file should live within the core module's test package. E.G: core > src > test > java > your > groupID > BaseTest.java
This BaseTest file should include any project specific spring wiring or configurations along with the Scaffold configuration. Make sure to also attach the annotation listed below, as well.
@Execution(ExecutionMode.CONCURRENT)
@ExtendWith(SpringExtension.class)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.NONE,
classes = { your_own_config.class, ScaffoldConfig.class }
)
public abstract class BaseTest extends ScaffoldBaseTest {
@Autowired
protected Navigation navigation;
@BeforeEach
public void doSomethingAtStart() {
// Some code here that might do something before each test is started
}
@AfterEach
public void doSomethingAtEnd() {
// Some code here that might do something after each test is finished
}
}
Let's break down what we see in this example above.
@SpringBootTest
annotation, be sure to change your_own_config.class
to your projects configuration class name.ScaffoldBaseTest
gets you the driver initialization and tear down and will also get you the WebDriverWrapper
instance from the current thread.@Component
with ease. Since it's protected, any class that extends off of BaseTest willBaseTest
file to perform any pre req or tear down actions outside of the driver initializing and closing.Spring profiles are sets of configurations that can be used when running testing locally or through a test automation framework like Sauce. These configurations determine the DesiredCapabilities
of the browser and can also
configure Sauce credentials. During a test run, you specify the spring profile to use. This will be explained a little bit later.
The spring profiles should live under the resources package in the environment module that was set up earlier in this guide. E.G: environment > src > main > resources > application-chrome_sauce.properties
. For more information on spring profiles, check out this link here.
To set the DesiredCapabilites
, include pre configured properties from the DesiredCapabilitiesConfigurationProperties
file. A full list of these properties can be found at the following link.
All of these properties are preceded by the prefix of desired-capabilities
. So, for example, if you wish to define the run type of sauce, you'd enter desired-capabilities.run-type=sauce
. Because Scaffold includes an
auto configuration for these properties, you gain the benefit of auto complete, as well. Simply type the first few letters of the word desired
will show you a list of capabilities that can be set.
If you'd like to learn more about desired capabilities, details on the configuration options that can be found here.
Below is the current list of potential desired capabilities to set.
# Browser/OS Config
desired-capabilities.run-type=where the browser testing is running, e.g. local, sauce, or grid
desired-capabilities.environment-type=the environment the testing is running on, e.g. test or stage.
desired-capabilities.browser-type=the browser type to be launched
desired-capabilities.browserVersion=the version of the browser to be launched
desired-capabilities.runPlatform=the operating system the browser is launching on
desired-capabilities.remote-url=The default grid URL to use
desired-capabilities.upload-screenshots=a boolean to determine if screenshots will be uploaded
One option of a test run could include a local execution. This type of configuration is good for a one off test to debug or for POC'ing a test. It's not recommended that you run a large suite of testing with a local browser. Typically, this sort of local configuration is included in an overrides spring profile and is not used for CI. If using an overrides profile do not include this file in your commit. Ensure that this file is added to your .gitignore. Overrides files are also used to include hard coded secrets and should never be exposed publicly.
Important note: Since a local browser is being used, you must have the local driver installed. E.G: ChromeDriver. Otherwise, the testing will not work.
For this example, let's assume we have created a new overrides profile. This overrides profile is called application-overrides.properties
. In that file, we would include the following:
base-environment-url=some_base_url
desired-capabilities.run-type=local
desired-capabilities.browser-type=chrome
desired-capabilities.run-platform=mac
Navigation
class.Another option of a test run could include a test execution against Sauce Labs. Because of the auto configuration defined by Scaffold, it's easy to add the sauce credentials to the overrides profile (to run the testing from your machine but sending the testing to sauce labs) or to a spring profile that is used in the CI/CD pipeline.
Important note: If adding the sauce credentials to an overrides file, ensure the overrides file is not checked in to your code base. These values should never be publicly exposed.
For this example, let's create a new spring profile called application-chrome_test.properties
. This configuration is going to be ran through sauce labs and will execute against a lower environment, TEST, with a windows browser.
This configuration file should be included would have the following:
base-environment-url=http://www.websitetest.com
# Sauce Config
desired-capabilities.sauce.url=<your sauce url>
desired-capabilities.sauce.user-name=<your username>
desired-capabilities.sauce.password=<your password>
desired-capabilities.sauce.access-key=<your access key>
desired-capabilities.sauce.tunnel-identifier=<your tunnel identifier>
# Base Desired Capabilities
desired-capabilities.run-type=sauce
desired-capabilities.browser-type=chrome
desired-capabilities.run-platform=windows
Navigation
class.Because of the hierarchy of the spring profile system, it is possible to create constant environment variable values that all spring profiles can automatically include. This is useful for sauce credentials since you can include the configuration in only one profile.
These constants can be included in a file named application.properties
. It's worth mentioning that any subsequent spring profile that sets the same environment variable will override the existing value in application.properties
.
This will allow you to set default values for any additional environment variable you create but override them in child profiles if you so wish.
For this example, let's say we'd like to include the sauce credentials in application.properties
. Since we might have multiple configurations we'd like to have, with all of them requiring sauce credentials, it makes sense to only
include them in one file.That file will include the following:
desired-capabilities.sauce.url=<your sauce url>
desired-capabilities.sauce.user-name=<your username>
desired-capabilities.sauce.password=<your password>
desired-capabilities.sauce.access-key=<your access key>
desired-capabilities.sauce.tunnel-identifier=<your tunnel identifier>
Taking the example from the Sauce Chrome Example section above, the new application-chrome_test.properties
file would look like this:
base-environment-url=http://www.websitetest.com
# Base Desired Capabilities
desired-capabilities.run-type=sauce
desired-capabilities.browser-type=chrome
desired-capabilities.run-platform=windows
Since the sauce credentials are already included in application.properties
, the application-chrome_test.properties
file does not need the sauce credentials. Therefore, we cut back on a little bit of code!
There are two potential methods of running the testing locally. The first is maven goal execution and the second is running the testing through the IDE.
During your maven goal execution step on your CI, specify the following system property: -Dspring.profiles.active=your_spring_profile.properties
.
This will pull the environment variables from the profile specified in the system property.
If you don't already have one, create a file named application.OVERRIDES
in environment > src > main > resources > application-OVERRIDES.properties
. In this file, include the desired capabilities and any other secret values required to run your testing.
NOTE: Make sure to add application.OVERRIDES
to your .gitignore
Next, create a new JUNIT run configuration for the testing you'd like to run locally. In the new run configuration, add the new environment variable SPRING.PROFILES.ACTIVE=OVERRIDES
. This setting will pull in the environment variables from the OVERRIDES profile when that test is run through the IDE.
As a reminder, it's recommended to not use a spring configuration that contains a local browser desired capability for running a large amount of testing.
Now your testing is ready to be executed.
During your maven goal execution step on your CI, specify the following system property: -Dspring.profiles.active=your_spring_profile.properties
.
This will pull the environment variables from the profile specified in the system property.