TestProject SDK for Java


Keywords
appium, opensdk, sdk, selenium, testproject
License
Apache-2.0

Documentation

TestProject OpenSDK For Java

Build Maven Central javadoc

TestProject is a Free Test Automation platform for Web, Mobile and API testing.
To get familiar with the TestProject, visit our main documentation website.

TestProject SDK is a single, integrated interface to scripting with the most popular open source test automation frameworks.

From now on, you can effortlessly execute Selenium and Appium native tests using a single automation platform that already takes care of all the complex setup, maintenance and configs.

With one unified SDK available across multiple languages, developers and testers receive a go-to toolset, solving some of the greatest challenges in open source test automation.

With TestProject SDK, users save a bunch of time and enjoy the following benefits out of the box:

  • 100% open source and available as a Maven dependency.
  • 5-minute simple Selenium and Appium setup with a single Agent deployment.
  • Automatic test reports in HTML/PDF format (including screenshots).
  • Collaborative reporting dashboards with execution history and RESTful API support.
  • Automatic distribution and deployment of test artifacts in case uploaded to the platform.
  • Always up-to-date with the latest and stable Selenium driver version.
  • A simplified, familiar syntax for both web and mobile applications.
  • Complete test runner capabilities for both local and remote executions, anywhere.
  • Cross platform support for Mac, Windows, Linux and Docker.
  • Ability to store and execute tests locally on any source control tool, such as Git.

Getting Started

To get started, you need to complete the following prerequisites checklist:

You must have Java Development Kit (JDK) 11 or newer installed.

Installation

For a Maven project, add the following to your pom.xml file:

<dependency>
  <groupId>io.testproject</groupId>
  <artifactId>java-sdk</artifactId>
  <version>1.2.3-RELEASE</version>
</dependency>

For a Gradle project, add the following to your build.gradle file:

compileOnly 'io.testproject:java-sdk:1.2.4-RELEASE'

Test Development

Using a TestProject driver is exactly identical to using a Selenium driver.
Changing the import statement is enough in most cases.

Following examples are based on the ChromeDriver, however are applicable to any other supported drivers.

Here's an example of how to create a TestProject version of ChromeDriver:

// import org.openqa.selenium.chrome.ChromeDriver; <-- Replaced
import io.testproject.sdk.drivers.web.ChromeDriver;

...

public class MyTest {
  ChromeDriver driver = new ChromeDriver(new ChromeOptions());
}

Here a complete test example:

Make sure to configure a development token before running this example.

package io.testproject.sdk.tests.examples.simple;

import io.testproject.sdk.drivers.web.ChromeDriver;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeOptions;

public final class WebTest {

    public static void main(final String[] args) throws Exception {
        ChromeDriver driver = new ChromeDriver(new ChromeOptions());

        driver.navigate().to("https://example.testproject.io/web/");

        driver.findElement(By.cssSelector("#name")).sendKeys("John Smith");
        driver.findElement(By.cssSelector("#password")).sendKeys("12345");
        driver.findElement(By.cssSelector("#login")).click();

        boolean passed = driver.findElement(By.cssSelector("#logout")).isDisplayed();
        if (passed) {
            System.out.println("Test Passed");
        } else {
            System.out.println("Test Failed");
        }

        driver.quit();
    }
}

Drivers

TestProject SDK overrides standard Selenium/Appium drivers with extended functionality.
Below is the packages structure containing all supported drivers:

io.testproject.sdk.drivers
├── web
│   ├── ChromeDriver
│   ├── EdgeDriver
│   ├── FirefoxDriver
│   ├── InternetExplorerDriver
│   ├── SafariDriver
│   └── RemoteWebDriver
├── android
│   └── AndroidDriver
├── ios
│   └── IOSDriver
└── GenericDriver

GenericDriver can be used to run non-UI tests.

Development Token

The SDK uses a development token for communication with the Agent and the TestProject platform.
Drivers search the developer token in an environment variable TP_DEV_TOKEN.
Token can be also provided explicitly using this constructor:

public ChromeDriver(final String token, final ChromeOptions options)

When a token is provided in both the constructor and an environment variable, the token in the environment variable will be used.

Remote Agent

By default, drivers communicate with the local Agent listening on http://localhost:8585.

Agent URL (host and port), can be also provided explicitly using this constructor:

public ChromeDriver(final URL remoteAddress, final ChromeOptions options)

It can also be set using the TP_AGENT_URL environment variable.

NOTE: By default, the agent binds to localhost. In order to allow the SDK to communicate with agents running on a remote machine (On the same network), the agent should bind to an external interface. For additional documentation on how to achieve such, please refer here

Remote (Cloud) Driver

By default, TestProject Agent communicates with the local Selenium or Appium server.
In order to initialize a remote driver for cloud providers such as SauceLabs or BrowserStack,
a custom capability cloud:URL should be set, for example:

SauceLabs
import io.testproject.sdk.drivers.web.ChromeDriver;
import io.testproject.sdk.drivers.TestProjectCapabilityType;

ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setCapability(
        TestProjectCapabilityType.CLOUD_URL,
        "https://{USERNAME}:{PASSWORD}@ondemand.us-west-1.saucelabs.com:443/wd/hub");
ChromeDriver driver = new ChromeDriver(chromeOptions);
BrowserStack
import io.testproject.sdk.drivers.web.ChromeDriver;
import io.testproject.sdk.drivers.TestProjectCapabilityType;

ChromeOptions chromeOptions = new ChromeOptions();
chromeOptions.setCapability(
        TestProjectCapabilityType.CLOUD_URL,
        "https://{USERNAME}:{PASSWORD}@hub-cloud.browserstack.com/wd/hub");
ChromeDriver driver = new ChromeDriver(chromeOptions);

Driver Builder

The SDK provides a generic builder for the drivers - DriverBuilder, for example:

ChromeDriver driver = new DriverBuilder<ChromeDriver>(new ChromeOptions())
  .withRemoteAddress(new URL("http://remote-agent-url:9999"))
  .withToken("***")
  .build(ChromeDriver.class);

Reports

TestProject SDK reports all driver commands and their results to the TestProject Cloud.
Doing so, allows us to present beautifully designed reports and statistics in it's dashboards.

Reports can be completely disabled using this constructor:

public ChromeDriver(final ChromeOptions options, final boolean disableReports)

There are other constructor permutations, refer to method summaries of each specific driver class.

Implicit Project and Job Names

The SDK will attempt to infer Project and Job names from JUnit / TestNG annotations.
If found the following logic and priorities take place:

  • Package name of the class containing the method is used for Project name.
  • JUnit 4 / 5
    • Class name or the @DisplayName annotation (JUnit 5 only) on the class is used for the Job name
    • Method name or the @DisplayName annotation (JUnit 5 only) on the method is used for the Test name(s)
  • TestNG
    • Class name or description from @BeforeSuite / @BeforeClass annotations if found, are used for the Job name
    • Method name or the @Test annotation (and it's testName / description fields) on the method is used for the Test name(s)

Examples of implicit Project & Job names inferred from annotations:

Explicit Names

Project and Job names can be also specified explicitly using this constructor:

public ChromeDriver(final ChromeOptions options, final String projectName, final String jobName)

For example:

ChromeDriver driver = new ChromeDriver(new ChromeOptions(), "My First Project", "My First Job");

Same can be achieved using the DriverBuilder:

ChromeDriver driver = new DriverBuilder<ChromeDriver>(new ChromeOptions())
  .withProjectName("My First Project")
  .withJobName("My First Job")
  .build(ChromeDriver.class);

Examples of explicit Project & Job names configuration:

Reporting Extensions

Reporting extensions extend the TestProject SDK reporting capabilities by intercepting unit testing framework assertions or exceptions thrown, and reporting them as failing steps.

JUnit5

In order to integrate it, a relevant interface needs to be implemented by your test class, for example:

import io.testproject.sdk.interfaces.junit5.ExceptionsReporter;
class MyTest implements ExceptionsReporter

Implementing this interface requires the class to implement the following method:

ReportingDriver getDriver();

This method's code, should provide access to the driver that can be used for the reporting.
Here is a complete example for a JUnit 5 based test.

TestNG

In order to integrate it, your test class will need to implement the relevant interface, in addition you will need to annotate your class with the TestNG @Listener annotation and direct it to the reporter from the SDK, for example:

import io.testproject.sdk.interfaces.testng.ExceptionsReporter;

@Listeners(io.testproject.sdk.internal.reporting.extensions.testng.ExceptionsReporter.class)
public class ExplicitReportTest implements ExceptionsReporter

Implementing this interface requires the class to implement the following method:

ReportingDriver getDriver();

The method should provide access to the driver that is used for reporting.

JUnit4

In order to integrate it, your test class will need to use the ExceptionsReportListener from the SDK.

To use it, annoate your test class with the @RunWith annotation and specify the ExceptionsReportListener class.

import io.testproject.sdk.internal.reporting.extensions.junit4.ExceptionsReportListener;

@RunWith(ExceptionsReportListener.class)
public class ExceptionsReportTest

Tests Reports

Automatic Tests Reporting

Tests are reported automatically when a test ends or when driver quits.
This behavior can be overridden or disabled (see Disabling Reports section below).

In order to determine that a test ends, call stack is traversed searching for an annotated methods.
When an annotated method execution starts and previously detected ends, test end is concluded.

Any unit testing framework annotations is reckoned, creating a separate test in report for every annotated method.
For example, following JUnit based code, will generate the following six tests in the report:

@BeforeEach
void beforeTestExample(TestInfo testInfo) {
    driver.report().step("Preparing Test: " + testInfo.getDisplayName());
}

@Test
@DisplayName(value = "Google")
void testGoogle() {
    driver.navigate().to("https://www.google.com/");
}

@Test
@DisplayName(value = "Yahoo!")
void testYahoo() {
    driver.navigate().to("https://yahoo.com/");
}

@AfterEach
void afterTestExample(TestInfo testInfo) {
    driver.report().step("Finishing Test: " + testInfo.getDisplayName());
}

Report:

Report
├── beforeTestExample
│   ├── Preparing Test: Yahoo!
├── Yahoo! Test
│   ├── Navigate To https://yahoo.com/
├── afterTestExample
│   ├── Finishing Test: Yahoo!
├── beforeTestExample
│   ├── Preparing Test: Google
├── Google Test
│   ├── Navigate To https://google.com/
└── afterTestExample
    └── Finishing Test: Google

See a complete example with automatic test reporting.

Limitations

JUnit5 dynamic test names cannot be inferred, and should be reported manually.
These will be reported as Dynamic Test when reported automatically.

Manual Tests Reporting

To report tests manually, use driver.report().tests() method and it's overloads, for example:

ChromeDriver driver = new ChromeDriver(new ChromeOptions());
driver.report().test("My First Test").submit();

It's important to disable automatic tests reporting when using the manual option to avoid collision.

Note that driver.report().test() returns a ClosableTestReport object.
An explicit call to submit() or closing the object is required for the report to be sent.

Using this closable object can be beneficial in the following case:

ChromeDriver driver = new ChromeDriver(new ChromeOptions());
try (ClosableTestReport report = driver.report().test("Example Test with Exception")) {
  driver.findElement(By.id("NO_SUCH_ELEMENT")).click();
}

Assuming there is no element on the DOM with such an ID: NO_SUCH_ELEMENT, an exception will be thrown and test will fail, but before that, closable object will get closed and test will be reported.

See a complete example with manual test reporting.

Steps

Steps are reported automatically when driver commands are executed.
If this feature is disabled, or in addition, manual reports can be performed, for example:

ChromeDriver driver = new ChromeDriver(new ChromeOptions());
driver.report().step("User logged in successfully");

Disabling Reports

If reports were not disabled when the driver was created, they can be disabled or enabled later.
However, if reporting was explicitly disabled when the driver was created, it can not be enabled later.

Disable all reports

Following will disable all types of reports:

ChromeDriver driver = new ChromeDriver(new ChromeOptions());
driver.report().disableReports(true);

Disable tests automatic reports

Following will disable tests automatic reporting.
All steps will reside in a single test report, unless tests are reported manually using driver.report().tests():

ChromeDriver driver = new ChromeDriver(new ChromeOptions());
driver.report().disableTestAutoReports(true);

Disable driver commands reports

Following will disable driver commands reporting.
Report will have no steps, unless reported manually using driver.report().step():

ChromeDriver driver = new ChromeDriver(new ChromeOptions());
driver.report().disableCommandReports(true);

Disable commands redaction

When reporting driver commands, SDK performs a redaction of sensitive data (values) sent to secured elements.
If the element is one of the following:

  • Any element with type attribute set to password
  • With XCUITest, on iOS an element type of XCUIElementTypeSecureTextField

Values sent to these elements will be converted to three asterisks - ***.
This behavior can be disabled as following:

ChromeDriver driver = new ChromeDriver(new ChromeOptions());
driver.report().disableRedaction(true);

Cloud and Local Report

By default, the execution report is uploaded to the cloud, and a local report is created, as an HTML file in a temporary folder.

At the end of execution, the report is uploaded to the cloud and SDK outputs to the console/terminal the path for a local report file:

Execution Report: {temporary_folder}/report.html

This behavior can be controlled, by requesting only a LOCAL or only a CLOUD report.

When the Agent is offline, and only a cloud report is requested, execution will fail with appropriate message.

Via a driver constructor:

ChromeDriver driver = new ChromeDriver(new ChromeOptions(), ReportType.LOCAL);

Or via the builder:

ChromeDriver driver = new DriverBuilder<ChromeDriver>(new ChromeOptions())
  .withReportType(ReportType.LOCAL)
  .build(ChromeDriver.class);

Control Path and Name of Local Reports

By default, the local reports name is the timestamp of the test execution, and the path is the reports directory in the agent data folder.

The SDK provides a way to override the default values of the generated local reports name and path.

Via constructor or via Driver Builder:

ChromeDriver driver = new DriverBuilder<ChromeDriver>(new ChromeOptions())
        .withReportName("Test Report - 1")
        .withReportPath("/tests/reports/")
        .build(ChromeDriver.class);

Logging

TestProject SDK uses SLF4J API for logging.
This means it only bind to a thin logger wrapper API, and itself does not provide a logging implementation.

Developers must choose a concrete implementation to use in order to control the logging output.
There are many SLF4J logger implementation choices, such as the following:

Note that each logger implementation would have it's own configuration format.
Consult specific logger documentation for on how to use it.

Using slf4j-simple

This is the simplest option that requires no configuration at all.
All needed is to add a dependency to the project:

Maven:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.30</version>
    <scope>test</scope>
</dependency>

Gradle:

testCompile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.30'

By default, logging level is set to INFO.
To see TRACE verbose messages, add this System Property -Dorg.slf4j.simpleLogger.defaultLogLevel=TRACE

Using Logback

Logback is a very popular logger implementation that is production ready and packed with many features.
To use it, add a dependency to the project:

Maven:

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
    <scope>test</scope>
</dependency>

Gradle:

testCompile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'

Create a new file src/main/resources/logback.xml in your project and paste the following:

<configuration>

    <!--Silence initial configuration logs-->
    <statusListener class="ch.qos.logback.core.status.NopStatusListener" />

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>
                %d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %-20logger{0} %message%n
            </Pattern>
        </layout>
    </appender>

    <logger name="io.testproject" level="ALL" />

    <root level="ERROR">
        <appender-ref ref="CONSOLE"/>
    </root>

</configuration>

Cucumber Framework

The SDK also supports automatic reporting of Cucumber features, scenarios and steps using the internal CucumberReporter plugin.

It will disable the reporting of driver commands and automatic reporting of tests. Instead, it will report:

  • A test for every scenario in a feature file
  • All steps in a scenario as steps in the corresponding test
  • Steps are automatically marked as passed or failed, to create comprehensive living documentation from your specifications on TestProject Cloud.

To use the plugin you must have an active TestProject driver instance before the execution of the features begins, either initializing the driver as the first step in your test or in a @BeforeClass annotated method.

Please note that while using the CucumberReporter, manual test reporting will be disabled.

If you are running your features via a JUnit Cucumber runner, you will need to specify the plugin from the @CucumberOptions annotation as seen below:

@RunWith(Cucumber.class)
@CucumberOptions(features = "path/to/feature/files",
        glue = "step.definitions.package",
        plugin = "io.testproject.sdk.internal.reporting.extensions.cucumber.CucumberReporter")
public class JunitTestRunner

If you are executing your features without a JUnit runner, you will need to specify the plugin in your in your program's Run/Debug configuration as :

--plugin io.testproject.sdk.internal.reporting.extensions.cucumber.CucumberReporter
If executing without a runner, it is possible to initialize
the driver as part of the step definition class constructor.

Addon Proxy

One of the greatest features of the TestProject platform is the ability to execute a code written by someone else. It can be your account colleagues, writing actions that you can reuse, or TestProject community users creating addons and solving common automation challenges.

To get started, download a source file with the proxy class for the Action(s) you want to execute. It can be done by navigating to the Addons page, opening an addon, and clicking on the Proxy link at the bottom left corner of the popup.

Now, let's pretend that one of your colleagues coded and uploaded an Addon naming it - Example Addon. To use it in your test, download its proxy source file, add it to your project and invoke the actions using the following driver method:

driver.addons().execute()

That expects an instance of the ActionProxy class. For example:

 // Use Addon proxy to invoke 'Clear Fields' Action
driver.addons().execute(JavaWebExampleAddon.getClearFieldsAction());

Following is an example of an element action invocation:

// Use Addon proxy to invoke 'Type Random Phone' Action
// Notice how the action parameters are provided using an action proxy convenience method
driver.addons().execute(
        JavaWebExampleAddon.typeRandomPhoneAction("44", 10),
        // Passing a 'By' instance, provides an element action with it's target
        By.cssSelector("#phone"));

Refer to the Addon Proxy Test for complete example source.

Package & Upload Tests to TestProject

Tests can be executed locally using the SDK, or triggered remotely from the TestProject platform.
Before uploading your Tests, they should be packaged into a JAR.

This JAR must contain all the dependencies (including TestProject SDK) and your unit tests (JUnit / TestNG).
Since unit Tests are not packaged by default, they must be included explicitly during build.

Gradle

Here's an example of additions to build.gradle that will create a JAR with dependencies and test classes:

build.gradle

jar {
    // Include compiled test classes and their sources
    from sourceSets.test.output+sourceSets.test.allSource

    // Collect and zip all classes from both test and runtime configurations
    from { configurations.testRuntimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
}

Maven

Here's an example of additions to pom.xml and a descriptor that will create a JAR with dependencies and test classes:

pom.xml

<build>
    <plugins>
        <!-- Use maven-jar-plugin to include tests -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>test-jar</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <!-- Use maven-assembly-plugin plugin with a custom descriptor -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <descriptors>
                    <!-- Path to the descriptor file -->
                    <descriptor>src/main/java/assembly/test-jar-with-dependencies.xml</descriptor>
                </descriptors>
            </configuration>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>


Assembly Descriptor

Save this file under src/main/java/assembly as test-jar-with-dependencies.xml:

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
    <id>test-jar-with-dependencies</id>
    <formats>
        <format>jar</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <dependencySets>
        <dependencySet>
            <outputDirectory>/</outputDirectory>
            <useProjectArtifact>true</useProjectArtifact>
            <useProjectAttachments>true</useProjectAttachments>
            <unpack>true</unpack>
            <scope>test</scope>
        </dependencySet>
    </dependencySets>
</assembly>

Uploading parameterized tests

TestProject allows running recorded tests using a dynamic parameter values using data-sources.
Same can be achieved using the OpenSDK as explained below.

Notes:

  1. Tests created with JUnit 4, do not support parameterization.
  2. Only String types are allowed as parameters!

Revealing parameter names

If you want your test parameter names to get displayed in TestProject platform, you must build your JAR with extra settings in order to expose them.

Gradle:

// Place anywhere in the build.gradle file
compileTestJava.options.compilerArgs.add '-parameters'

Maven:

<!-- If you already have a compiler plugin, just add the compilerArgs tag. -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.7.0</version>
    <configuration>
        <source>11</source>
        <target>11</target>
        <encoding>UTF-8</encoding>
        <compilerArgs>
            <arg>-parameters</arg>
        </compilerArgs>
    </configuration>
</plugin>

To allow parameterization with JUnit 5, OpenSDK provides a special data-source class: TestProjectParameterizer.

This class expects the TestProject Agent to set a path to a CSV data-source file, in runtime. However during debugging or when running the test locally, it will throw an exception saying that No data provider was specified..

To avoid this, there are two options:

  • Use a hard-coded @ValueSource annotation that should be removed before packaging the JAR file and uploading to TestProject.
  • Specify a path to a local CSV file using the TP_TEST_DATA_PROVIDER environment variable, and mimic Agent's behavior.

JUnit 5

Here's a simple example of a parameterized test using JUnit5:

import io.testproject.sdk.drivers.web.ChromeDriver;
import io.testproject.sdk.interfaces.parameterization.TestProjectParameterizer;
import io.testproject.sdk.internal.exceptions.AgentConnectException;
import io.testproject.sdk.internal.exceptions.InvalidTokenException;
import io.testproject.sdk.internal.exceptions.ObsoleteVersionException;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeOptions;

import java.io.IOException;
public class JUnit5Example {
    @ParameterizedTest
    @ArgumentsSource(TestProjectParameterizer.class)
    public void paramTest(String username, String password)
            throws InvalidTokenException, AgentConnectException, ObsoleteVersionException, IOException {
        ChromeDriver driver = new ChromeDriver(new ChromeOptions());

        // Navigate to TestProject Example website
        driver.navigate().to("https://example.testproject.io/web/");

        // Login using provided credentials
        driver.findElement(By.cssSelector("#name")).sendKeys(username);
        driver.findElement(By.cssSelector("#password")).sendKeys(password);
        driver.findElement(By.cssSelector("#login")).click();
        driver.quit();
    }
}

TestNG

Here's a simple example of a parameterized test using TestNG:

import io.testproject.sdk.drivers.web.ChromeDriver;
import io.testproject.sdk.interfaces.parameterization.TestProjectParameterizer;
import io.testproject.sdk.internal.exceptions.AgentConnectException;
import io.testproject.sdk.internal.exceptions.InvalidTokenException;
import io.testproject.sdk.internal.exceptions.ObsoleteVersionException;
import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeOptions;
import org.testng.annotations.Test;

import java.io.IOException;

public class TestNGExample {
    @Test(dataProvider = "TestProject", dataProviderClass = TestProjectParameterizer.class)
    public void paramTest(final String username, final String password)
            throws InvalidTokenException, AgentConnectException, ObsoleteVersionException, IOException {
        ChromeDriver driver = new ChromeDriver(new ChromeOptions());

        // Navigate to TestProject Example website
        driver.navigate().to("https://example.testproject.io/web/");

        // Login using provided credentials
        driver.findElement(By.cssSelector("#name")).sendKeys(username);
        driver.findElement(By.cssSelector("#password")).sendKeys(password);
        driver.findElement(By.cssSelector("#login")).click();

        driver.quit();
    }
}

Addons

An Addon is a collection of actions you can use within any recorded test to extend the recorder’s capabilities.
Addons can be element based or non-UI based:

  • Element based Addons provide extended functionalities on customized UI elements.
  • Non-UI based Addons combine steps within your recorded tests, such as: File operations, REST API commands, image comparison, etc.

There are hundreds of Addons to choose from, or you can build your own.

Addons are developed in Java and uploaded to a collaborative library to the user’s account.
Once uploaded, all team members in the account can use the Addons as part of their tests.

TestProject’s Agent automatically distributes Addons based on the account member’s usage.
You can update new versions for Addons, and add more actions or change their functionality according to your needs (All tests using the newly versioned Addon will be updated as well).

There are two types of Addons:

  1. Community Addons: Community Addons are shared by the entire TestProject community and give you the power to effortlessly extend your tests while saving valuable time.
    • The usage of community addon is identical to account addons: Once the community-based action is selected within the Smart Test Recorder, the Addon is automatically downloaded and installed to the account.
    • Before being shared with the entire community, the TestProject team reviews the code and approves the Addon for public usage.
  2. Account Private Addons: Account Addons are private and only accessible to account team members. These Addons do not need to be approved by TestProject before the upload/usage.

Addon development

Developing an addon is a very simple process. It can be done in 4 easy step:

  1. Implement the addon.
  2. Test your addon locally.
  3. Download a manifest.
  4. Upload to TestProject.

Implement the Addon

Lets review a simple Addon with a ClearFields action that clears a form.
It can be used on the login form in TestProject Demo website or mobile App.

package io.testproject.sdk.tests.examples.addons.actions;

import io.testproject.sdk.internal.addons.annotations.AddonAction;
import io.testproject.sdk.internal.addons.interfaces.Action;
import io.testproject.sdk.internal.addons.Platform;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.WebDriver;

@AddonAction(platforms = Platform.Web, name = "Clear Fields")
public final class ClearFields implements Action<WebDriver> {
    @Override
    public boolean run(final WebDriver driver) {
        // Search for Form elements
        for (WebElement form : driver.findElements(By.tagName("form"))) {

            // Ignore invisible forms
            if (!form.isDisplayed()) {
                continue;
            }

            // Clear all inputs
            for (WebElement element : form.findElements(By.tagName("input"))) {
                element.clear();
            }
        }

        return true;
    }
}

Addon Actions

In order to build an Action that can be executed by TestProject, the class has to implement one of the interfaces that the SDK provides.\

Interface implementation requires an implementation of the run() method, that will be be invoked by the platform to run the Action.
The run() method returns boolean indicating if the action is passed. It will also fail if an exception is thrown from the action's code.

Web Action

package io.testproject.sdk.tests.examples.addons.actions;

import io.testproject.sdk.internal.addons.annotations.AddonAction;
import io.testproject.sdk.internal.addons.interfaces.Action;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.WebDriver;
import io.testproject.sdk.internal.addons.Platform;

/**
 * Clears fields in a web application.
 * This action can work for all browsers and must therefore use selenium's RemoteWebDriver as its driver type.
 * To prevent it from being available for mobile, we add the AddonAction annotation.
 */
@AddonAction(Platforms = Platform.Web, name = "Clear Fields")
public final class ClearFields implements Action<WebDriver> {
    @Override
    public boolean run(final WebDriver driver) {
        // Search for Form elements
        for (WebElement form : driver.findElements(By.tagName("form"))) {

            // Ignore invisible forms
            if (!form.isDisplayed()) {
                continue;
            }

            // Clear all inputs
            for (WebElement element : form.findElements(By.tagName("input"))) {
                element.clear();
            }
        }

        return true;
    }
}

Android Action

package io.testproject.sdk.tests.examples.addons.actions;

import io.testproject.sdk.drivers.android.AndroidDriver;
import io.testproject.sdk.internal.addons.interfaces.Action;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;

/**
 * Clears all edit text boxes in android.
 * This action can run on android only and therefore supports only AndroidDriver.
 * However, it doesn't require a specific annotation since the driver is specific enough.
 */
public final class ClearFields implements Action<AndroidDriver<WebElement>> {
    @Override
    public boolean run(final AndroidDriver<WebElement> driver) {
        for (WebElement element : driver.findElements(By.className("android.widget.EditText"))) {
            element.clear();
        }

        return true;
    }
}

IOS Action

package io.testproject.sdk.tests.examples.addons.actions;

import io.testproject.sdk.drivers.android.IOSDriver;
import io.testproject.sdk.internal.addons.interfaces.Action;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;

/**
 * Clears all edit text boxes in android.
 * This action can run on ios only and therefore supports only IOSDriver.
 * However, it doesn't require a specific annotation since the driver is specific enough.
 */
public final class ClearFields implements Action<IOSDriver<WebElement>> {
    @Override
    public boolean run(final IOSDriver<WebElement> driver) {
        for (IOSElement element : helper.getDriver().findElements(By.className("XCUIElementTypeTextField"))) {
            element.clear();
        }

        for (IOSElement element : helper.getDriver().findElements(By.className("XCUIElementTypeSecureTextField"))) {
            element.clear();
        }

        for (IOSElement element : helper.getDriver().findElements(By.className("XCUIElementTypeSearchField"))) {
            element.clear();
        }

        return true;
    }
}

Element Actions

Actions can be element based, when their scope is limited to operations on a specific element and not the whole DOM.
This allows creating smart crowd based addons for industry common elements and libraries.
The action receives the element search criteria and has to search for the element on its own.

TypeRandomPhone is an example of an Element Action:

package io.testproject.sdk.tests.examples.addons.actions;

import io.testproject.sdk.internal.addons.annotations.AddonAction;
import io.testproject.sdk.internal.addons.interfaces.Action;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.WebDriver;

/**
 * Types a random phone number in the requested element.
 * This action is the same for all platforms, and therefore uses Selenium's WebDriver with no annotations.
 * There is no need to implement this action more than once.
 */
public final class TypeRandomPhoneNumber implements Action<WebDriver> {
    @Override
    public boolean run(final WebDriver driver, final By elementSearchCriteria) {
        long number = (long) (Math.random() * Math.pow(10, 7));
        phone = String.format("+1%s", number);
        WebElement element = helper.getDriver().findElement(elementSearchCriteria);
        element.sendKeys(phone);
        return true;
    }
}

Generic Actions

Some actions do not require a driver to work. We can run such actions on any platform:

package io.testproject.sdk.tests.examples.addons.actions;

import io.testproject.sdk.internal.addons.interfaces.GenericAction;
import io.testproject.sdk.drivers.ReportingDriver;

/**
 * Types a random phone number in the requested element.
 * This action is the same for all platforms, and therefore uses Selenium's RemoteWebDriver with no annotations.
 * There is no need to implement this action more than once.
 */
public final class Addition implements GenericAction {
    @Parameter(direction = ParameterDirection.OUTPUT)
    public int total;

    @Override
    public boolean run(final ReportingDriver driver) {
        // If this was a real action, a and b would be input parameters.
        int a = 1;
        int b = 2;
        total = a + b;
        return true;
    }
}

Action Annotations

TestProject SDK provides annotations to describe the action:

  1. The AddonAction annotation is used to better describe your action and define how it will appear later in TestProject UI: \
    • platforms - A mandatory field that explains which platform this action applies to.
    • name - The name of the action (if omitted, the name of the class will be used).
    • description - A description of the test which is shown in various places in TestProject platform (reports for example). The description can use placeholders {{propertyName}} do dynamically change the text according to test properties.
    • version - A version string which is used for future reference.
  2. The Parameter annotation is used to better describe your action's inputs and outputs, in the example above there are two parameters - question and answer.
    • description - The description of the parameter
    • direction - Defines the parameter as an input (default if omitted) or an output parameter. An input parameter will able to receive values when it is being executed while the output parameter value will be retrieved at the end of test execution (and can be used in other places later on in the automation scenario).
    • defaultValue - Defines a default value that will be used for the parameter.

Debugging / Running Actions

Actions run in context of a test and assume that required UI state is already in place.
When the action will be used in a test it will be represented as a single step, usually preceded by other steps.
However, when debugging it locally, preparations should be done by creating a driver to start from expected UI state:

Web - State Preparation

// Create Action
ClearFields action = new ClearFields();

// Initialize Driver - using chrome but this will work on any web driver.
ChromeDriver driver = new ChromeDriver(new ChromeOptions()));

// Prepare state
driver.navigate().to("https://example.testproject.io/web/");
driver.findElement(By.cssSelector("#name")).sendKeys("John Smith");
driver.findElement(By.cssSelector("#password")).sendKeys("12345");

// Run action
driver.addons().run(action);

Android - State Preparation

// Create Action
ClearFields action = new ClearFields();

// Initialize Driver
DesiredCapabilities capabilities = new DesiredCapabilities();

capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID);
capabilities.setCapability(MobileCapabilityType.UDID, "{YOUR_DEVICE_UDID}");
capabilities.setCapability(CapabilityType.BROWSER_NAME, "");
capabilities.setCapability(MobileCapabilityType.APP, "https://github.com/testproject-io/android-demo-app/raw/master/APK/testproject-demo-app.apk");

AndroidDriver<MobileElement> driver = new AndroidDriver<>(capabilities);

// Prepare state
driver.findElement(By.id("name")).sendKeys("John Smith");
driver.findElement(By.id("password")).sendKeys("12345");

// Run action
driver.addons().run(action);

iOS - State Preparation

// Create Action
ClearFields action = new ClearFields();

// Initialize Driver
DesiredCapabilities capabilities = new DesiredCapabilities();

capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS);
capabilities.setCapability(MobileCapabilityType.UDID, "{YOUR_DEVICE_UDID}");
capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "{YOUR_DEVICE_NAME}");
capabilities.setCapability(CapabilityType.BROWSER_NAME, "");

// Compile and deploy the App from source https://github.com/testproject-io/ios-demo-app
capabilities.setCapability(IOSMobileCapabilityType.BUNDLE_ID, "io.testproject.demo");
IOSDriver<WebElement> driver = new IOSDriver<>(capabilities);

// Prepare state
driver.findElement(By.id("name")).sendKeys("John Smith");
driver.findElement(By.id("password")).sendKeys("12345");

// Run action
driver.addons().run(action);

Addon Manifest

To upload an Addon a manifest file is required.
The manifest is a descriptor of your Addon, it contains a unique GUID for the addon and a list of required permissions.
Create an Addon in the Addons screen and download the generated manifest, placing it in your project resources folder.

Package & Upload your Addon

Tests can be executed locally using the SDK, or triggered remotely from the TestProject platform.
Before uploading your Tests, they should be packaged into a JAR.

This JAR must contain all the dependencies (except for the TestProject SDK) and your action, but not your tests.
Since dependencies are not packaged by default, they must be included explicitly during build.

Gradle

Here's an example of additions to build.gradle that will create a JAR with dependencies and test classes:

build.gradle

dependencies {
    // Add TestProject SDK for compilation only.
    compileOnly 'io.testproject:java-sdk:1.2.3-RELEASE'
    
    // Other dependencies go here
}

jar {
    // Collect & zip all dependencies (except OpenSDK)
    from {
        (configurations.runtimeClasspath).collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }

    // Extract SDK version
    from {
        (configurations.compileClasspath).collect {
            zipTree(it).matching {
                include 'testproject-opensdk.properties'
            }
        }
    }
}

Maven

Here's an example of additions to pom.xml and a descriptor that will create a JAR with dependencies and test classes:

pom.xml

<build>
    <plugins>
        <!-- Use maven-jar-plugin to include tests -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <executions>
                <execution>
                    <goals>
                        <goal>test-jar</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <!-- Use maven-assembly-plugin plugin with a custom descriptor -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-assembly-plugin</artifactId>
            <configuration>
                <descriptors>
                    <!-- Path to the descriptor file -->
                    <descriptor>src/main/java/assembly/jar-with-dependencies.xml</descriptor>
                </descriptors>
            </configuration>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>single</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>


Assembly Descriptor

Save this file under src/main/java/assembly as jar-with-dependencies.xml:

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
    <id>jar-with-dependencies</id>
    <formats>
        <format>jar</format>
    </formats>
    <includeBaseDirectory>false</includeBaseDirectory>
    <dependencySets>
        <dependencySet>
            <outputDirectory>/</outputDirectory>
            <useProjectArtifact>true</useProjectArtifact>
            <useProjectAttachments>true</useProjectAttachments>
            <unpack>true</unpack>
            <scope>runtime</scope>
        </dependencySet>
    </dependencySets>
</assembly>

Examples

Here are more examples:

License

TestProject SDK For Java is licensed under the LICENSE file in the root directory of this source tree.