Building Selenium framework in java (part II) - do it right for the first time

In this article I would like to share with you how I build new end to end testing solutions for web projects. I’ll try to explain how well thought test framework architecture can improve test code quality.

|

Introduction

In this article I would like to share with you how I build new end to end testing solutions for web projects. I’ll try to explain how well thought test framework architecture can improve test written like this:

public void searchTest() {
    WebDriver driver = new FirefoxDriver();
    driver.get("http://www.google.com");
    WebElement element = driver.findElement(By.name("q"));
    element.sendKeys("Cheese!");
    element.submit();
    System.out.println("Page title is: " + driver.getTitle());
    (new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() {
        public Boolean apply(WebDriver d) {
            return d.getTitle().toLowerCase().startsWith("cheese!");
        }
    });
    System.out.println("Page title is: " + driver.getTitle());
    driver.quit();
}

…and make them look more in that way:

public void searchTest() {
    new SearchPage(getDriver())
            .setSearchPhrase("Cheese!")
            .submitForm()
        .assertThat()
            .pageTitleContains("cheese!");
}

For demo purposes I decided to build my testing solution by using Maven and TestNG.

Architecture

Our framework will have three layers architecture to separate what we want to test (test layer) from how we actually do it (business layer). By using such approach it is relatively easy to replace code responsible for communication with browser, if anything changes in product UI or we would like to switch from Selenium to something else – tests would stay the same as long as application business logic doesn’t change.

We need to create instance of WebDriver to communicate with web browser. This is definitely part of core layer and specific driver implementation should be hidden from test . In that way we will be able to run the same tests for different browsers.

WebDriver is going to be used by Page Objects, which actually represent our product UI. Keep in mind that it is not said that each product web page needs to have single page object assigned. It is quite common that many page objects need to be created to fullfill business logic on single web page – especially, when there is asynchronously modified content. Abstract definition of Page is going to be part of core layer when Page Object implementations will be in business layer.

Scenarios will be used to group page object method calls in some portions reusable from product business logic perspective. Thanks to them we will make our test methods shorter to keep focus on what in fact is to be tested by given test. Scenario interface goes to core layer and Scenario implementations are part of business layer.

Test methods will be responsible to manage test data and distribute it to pages and scenarios to achieve some state of the system under test. Assertions should be called directly from test layer – do not hide them in pages or scenarios to avoid asserting same thing many times.

Additionally I would like to create test methods which will contain only one chain of methods to make them extreamly clean. To do that I will prepare special assertion mechanism, which can be used together with pages method chaining. Tests and assertions are part of test layer.

In the end of the article you will find a link to basic test framework with all those features implemented.

Dependencies

Despite the fact that to make everything work we actually need only two dependencies – Selenium and TestNG, I suggest to add at least two more:

  • Lombok – it will remove all boilerplate code from your project – it works perfectly with page objects pattern approach.
  • WebDriverManager – it smoothly manages WebDriver binaries so you don’t need to worry about downloading them manually from every browser vendor.
<dependency>
    <groupId>org.seleniumhq.selenium</groupId>
    <artifactId>selenium-java</artifactId>
    <version>${selenium-java.version}</version>
</dependency>
<dependency>
    <groupId>org.testng</groupId>
    <artifactId>testng</artifactId>
    <version>${testng.version}</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
</dependency>
    <groupId>io.github.bonigarcia</groupId>
    <artifactId>webdrivermanager</artifactId>
    <version>${webdrivermanager.version}</version>
</dependency>

WebDriver

WebDriver is an interface to communicate with a browser. There is couple of things worth mentioning if it comes to creating new instances of WebDriver:

  • Avoid using static collections of WebDrivers – ideally when for each test there is new instance created dynamically – this guarantees tests independence and as a result they can be easily run in parallel (Selenium Grid).
  • Use well known Factory design pattern to generate instances for different browsers.
  • This part of the framework definitely belongs to core layer – separate it from other layers.
  • Make use of WebDriverManager to magically download and setup driver configuration.

Sample WebDriver factory class:

public class WebDriverFactory {
    private WebDriverFactory() {
    }

    public static WebDriver getDriver(DriverType browser) {
        WebDriver driver = null;
        switch (browser) {
            case FIREFOX:
                driver = getFirefoxDriver();
                break;
        }
        return driver;
    }

    private static WebDriver getFirefoxDriver() {
        FirefoxDriverManager.getInstance().setup();
        return new FirefoxDriver();
    }
}

Page Objects

Page Objects act as a proxy during communication with UI from test perspective. While the concept is quite straightforward, here comes some ideas you may want to consider:

  • Keep page object stateless - pages act as proxy and should not store any testing data, actual state of the system or assertions.
  • Create an abstract Page class, which would be another part of the framework core layer – make sure all your page object classes extend this class, where you could place logic common for every page, like PageFactory initialization:
    public abstract class Page {
        protected WebDriver driver;
    
        public Page(WebDriver driver) {
            this.driver = driver;
            PageFactory.initElements(new DefaultElementLocatorFactory(driver), this);
        }
    }
    
  • Use method chaining  – every page method should return  page object instance – this is quite strict approach, but thanks to fluent interface you will have clean code and extremely clear test methods.
  • Determine method naming convention – list of allowed verbs, nouns and in what order they need to be used – it helps test developers to find already created methods.
  • Don’t mess your code with unnecessary logging – try using aop (like AspectJ) together with some logging library (like logback) to automatically log page method calls during test to keep track of test flow.

Sample Page class:

@Getter
public class LoginPage extends BasePage {
    @FindBy(id = "user_email")
    private WebElement emailField;
    @FindBy(id = "user_password")
    private WebElement passwordField;
    @FindBy(css = ".btn-primary")
    private WebElement loginButton;

    public LoginPage(WebDriver driver) {
        this(driver, null);
    }
    public LoginPage(WebDriver driver, String url) {
        super(driver, url);
    }

    public LoginPage setEmail(String email) {
        emailField.sendKeys(email);
        return this;
    }

    public LoginPage setPassword(String password) {
        passwordField.sendKeys(password);
        return this;
    }

    public DashboardPage clickLogin() {
        loginButton.click();
        return new DashboardPage(driver);
    }
}

Scenarios

By now you should be able to create nice testing flows, which will follow your application behavior, like this:

new LoginPage(getDriver())
        .setEmail(user.getEmail())
        .setPassword(user.getPassword())
        .clickLogin();

It looks quite cool already, however we can improve it a little bit. Let’s assume you want to create independent test and each of them requires login at the beginning. It would be nice to not rewrite whole login logic every time, right? We can create scenario classes, which will help us making test methods shorter and not repeat code that much:

  • Create a scenario interface as a part of framework core:
    public interface Scenario<Input extends Page, Output extends Page> {
        Output run(Input entry);
    }
    
    It takes input page and output page as generic parameters. In that way we can simply replace parts of methods chain by implementations of Scenario interface.
  • For logical integrity make sure that every scenario use only one method chain and implement single business action.
    Sample Scenario class:
    @AllArgsConstructor
    public class LoginScenario implements Scenario<LoginPage, DashboardPage> {
        private BaseUser user;
    
        @Override
        public DashboardPage run(LoginPage loginPage) {
            return loginPage
                .setEmail(user.getEmail())
                .setPassword(user.getPassword())
                .clickLogin();
        }
    }
    

Assertions

There is one last piece to add in the puzzle – assertions. As I mentioned at the beginning I want to create mechanism, which will allow creating test with single fluent interface flow only. I also wrote that it is not recommended to put assertions in page or scenario classes. So here is how I am doing it:

  • Create assertion interface:
    public interface ISupportAssertions {
    
        <G extends Page, T extends Assertions<G>> T check(Class<T> clazz);
    
    }
  • Make abstract Page class an implementation of that interface and add following method:
    @Override
    public <G extends Page, T extends Assertions<G>> T check(Class<T> clazz) {
        try {
            Assertions<G> assertion = clazz.newInstance();
            assertion.setPage((G)this);
            return (T) assertion;
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("Error occur during creating Assertions.", e);
        }
    }
  • Add assertion abstraction, which will store page object to test:
    public abstract class Assertions {
    
        protected T page;
    
        public T endAssertion() {
            return page;
        }
    
        public void setPage(T page) {
            this.page = page;
        }
    }

Now all page object implementations automatically has ability to use assertion classes inside of fluent interface by calling check() method. TestNG assertions are used inside of assertion class methods, which have access to page objet fields via getters generated by lombok.

Sample assertion class:

public class LeadDetailsAssertions extends Assertions<LeadDetailsPage> {

    public LeadDetailsAssertions verifyStatus(String name) {
        assertEquals(page.getStatus().getText(), name,
            "Lead status displayed in lead details page is not as expected. ");
        return this;
    }
}

Test Methods

To be able to run our tests, we need a place where WebDriver would be initialized. This can be done in abstract Test class, which later on could be extended by all test classes:

@Listeners(TestListener.class)
public abstract class SeleniumTest {
    private WebDriver driver; 

    @BeforeClass(alwaysRun = true)
    public void classSetup() {
        driver = WebDriverFactory.getDriver(
            DriverType.valueOf(System.getProperty("driver", "chrome").toUpperCase()));
    }
}

At this point we can create nicely looking test methods – one fluent interface flow, self-explanatory scenario, single assertion:

@Test
public void verifyInterviewScenarioTest() {
    new LoginPage(getDriver(), getConfig().getBaseUrl())
        .run(new LoginScenario(user))
        .run(new LeadCreateScenario(lead))
            .check(LeadDetailsAssertions.class)
                .verifyStatus(newStatus.getName());
}

Summary

Presented architecture is definitely not the simplest in the world, but brings all advantages I care about when creating and using test framework:

  • Strict, documented rules makes it easy to create new tests for everyone
  • Fluent interface API guides you through the product functionalities
  • Scalable solution when system under test grows
  • Easy maintenance

To see complete solution in work, please visit my github repo https://github.com/bdrzew/basetest.

What’s next?

In next article I will try to explain if it’s worth to introduce cucumber into already existing test framework and actually share some code to show how it can be done to reuse fluent interface and page object pattern approach.

Bartek Drzewiński

Did you like this article?

Building Selenium framework in java (part II) - do it right for the first time