Writing good Selenium tests with Page objects and Spring

I have to admit, a couple of years ago I wasn’t a huge fan of Selenium. The project I’m selenium-logoassigned to is a quite large and complex customer web portal and has always been in need of automated GUI checks. Several attempts have been made over the years, each failing miserably. I believe the worst one was when a team of off-site contractors was hired to create a proof of concept. They worked hard for months, struggling with the business logic and page design and came up with a huge set of tests. Some months later we threw them all away.

The reason they failed? The tests weren’t readable so we couldn’t easily understand or extend them and they became a nightmare to maintain. They were written in the classic script form were every other line is either a “driver.findElement(…)” or “element.sendKeys(…)”, obscuring the purpose of the test. Basically they resembled the output from the Selenium IDE Firefox plugin. I have often been told that it’s a good thing that everyone can create tests. That a person with little or no programming skill can record their actions when they interact with the site and turn this into a test case. I fail to comprehend this. I don’t believe there is any value in creating large amounts of tests in little time. The value is when you have a suite of well written, easy to understand test cases that can be maintained at a low cost. And that can only be accomplished by skilled programmers.

When I came back from a couple of months of paternity leave, my team had decided to make a new attempt at creating Selenium tests for the product. Since the whole team was committed I decided to support the effort. But I was also determined to come up with a design where we could write readable and maintainable tests, composed of reusable parts. That’s when I stumbled upon the Page object pattern.

The Page object pattern is a design pattern where the logic of navigating and interacting with the site is separated from the actual test code. Each page on the site is represented by a class containing the logic for locating the elements on that page. The pattern can also be applied to parts of pages, where a page object can represent for example a lightbox. The Selenium framework supports this pattern by providing a utility for setting up page objects,  the PageFactory class. This class uses reflection to find all fields of type WebElement in a page object. Each field is proxied allowing for a lookup of the corresponding HTML element when the field is accessed. By default, the name of the field is matched against the id of the HTML element, but the field can also be annotated with an @FindBy expression for matching against class names, css, xpath, etc.

Applying the pattern to our tests really cleaned them up but I wanted to take things one step further by also applying dependency injection. By doing this I could remove the boilerplate code of calling the PageFactory in each page object. My page objects became singleton beans which could be injected into test classes and into other page objects.

The team approved of my design and just last week we refactored the last of the old tests to use the new framework. We now have a good set of readable tests and we have page objects covering most of the functionality on the site, meaning it’s very cheap to add new tests. I will write a follow-up to this post explaining the other cool stuff we have done with the new framework.

Creating the framework

The framework we have today has a lot of extra features not mentioned above, but I will try to extract the basic classes needed for working with page objects and Spring. I start by creating a base class for my Selenium test cases. This is mostly a standard Spring context test case, but it also adds my own test execution listener.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SeleniumTestCaseContext.class)
@TestExecutionListeners({ SeleniumTestExecutionListener.class,
 DependencyInjectionTestExecutionListener.class })
public abstract class SeleniumTestCase {}

The Spring application context configuration is simple. Most beans are created from a component scan, but I also create a Selenium Firefox driver bean since I want to be able to inject it into my page objects. Drivers can’t be reused between tests so I have created a custom scope for the driver bean called “test”. This scope will create a new bean before each test.

@Configuration
@ComponentScan(basePackageClasses = SeleniumTestCaseContext.class)
public class SeleniumTestCaseContext {

 @Bean
 public TestScope testScope() {
  return new TestScope();
 }

 @Bean
 public CustomScopeConfigurer customScopeConfigurer() {
  CustomScopeConfigurer scopeConfigurer = new CustomScopeConfigurer();
  Map<String, Object> scopes = new HashMap<>();
  scopes.put("test", testScope());
  scopeConfigurer.setScopes(scopes);
  return scopeConfigurer;
 }

 @Bean
 @Scope("test")
 public WebDriver webDriver() {
  return new FirefoxDriver();
 }
}

The test scope implementation is trivial. It holds a map where test scoped beans are cached, ensuring that only one instance is created. It also has a reset method for clearing the cache before each test run.

public class TestScope implements Scope {

 private Map<String, Object> cache = new HashMap<>();

 public void reset() {
  cache.clear();
 }

 @Override
 public Object get(String name, ObjectFactory<?> objectFactory) {
  if (!cache.containsKey(name)) {
   cache.put(name, objectFactory.getObject());
  }
  return cache.get(name);
 }

 @Override
 public Object remove(String name) {
  return cache.remove(name);
 }

 @Override
 public void registerDestructionCallback(String name, Runnable callback) {}

 @Override
 public Object resolveContextualObject(String key) {
  return null;
 }

 @Override
 public String getConversationId() {
  return null;
 }
}

Next up is the test execution listener which was registred in the base test case above. The listener has two responsibilities: resetting the test scope before each test and closing the Selenium driver, i.e. the browser, after each test.

public class SeleniumTestExecutionListener extends AbstractTestExecutionListener {

 @Override
 public void beforeTestMethod(TestContext testContext) throws Exception {
  testContext.getApplicationContext().getBean(TestScope.class).reset();
 }

 @Override
 public void afterTestMethod(TestContext testContext) throws Exception {
  testContext.getApplicationContext().getBean(WebDriver.class).quit();
 }
}

My page objects are also Spring beans and I create my own @Component annotation for them.

@Target(TYPE)
@Retention(RUNTIME)
@Component
public @interface PageObject {}

The final step is to have Selenium initialize my page beans, i.e. to proxy all WebElement fields in them. This initialization is done in my bean post processor.

@Component
public class PageObjectBeanPostProcessor implements BeanPostProcessor {

 @Autowired
 private WebDriver driver;

 @Override
 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
  if (bean.getClass().isAnnotationPresent(PageObject.class)) {
   PageFactory.initElements(driver, bean);
  }
  return bean;
 }

 @Override
 public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
  return bean;
 }
}

I now create two page objects: Site which is the root page object for accessing other pages and AccountPage which is a page with a simple form.

@Page
public class Site {

 @Autowired
 private WebDriver driver;

 @Autowired
 private AccountPage accountPage;

 public AccountPage gotoAccountPage() {
  driver.get("http://localhost/account.html");
  return accountPage;
 }
}

@Page
public class AccountPage {

 private WebElement firstName;
 private WebElement lastName;

 @FindBy(css = "input[type='submit']")
 private WebElement submitButton;

 public AccountPage firstName(String text) {
  firstName.clear();
  firstName.sendKeys(text);
  return this;
 }

 public AccountPage lastName(String text) {
  lastName.clear();
  lastName.sendKeys(text);
  return this;
 }

 public AccountPage submit() {
  submitButton.click();
  return this;
 }
}

I like giving my page objects a fluent interface making it easy to chain stuff like filling out and submitting a form. The last step is of course to create a test case.

public class PageTest extends SeleniumTestCase {

 @Autowired
 private Site site;

 @Test
 public void test() {
  AccountPage accountPage = site.gotoAccountPage();
  accountPage.firstName("Peter").lastName("Kedemo").submit();
 }
}

Even though this isn’t much of a test, it should be clear how easy it is to create readable tests with this approach.

10 thoughts on “Writing good Selenium tests with Page objects and Spring

  1. Johan Sjöberg (@johansjoberg_)

    Hi, as one of the two people hired for writing the “horrible pilot project” using selenium, I wanted to give my 2 cents on the matter. Before I jabber on about why the project failed, I would however like to say that I agree on using the Page pattern to make test more readable. I would also like to suggest a look at the FluentLenium framework that seem to do some of the things you solved using Spring. The drawback however is that you need to inherit from their classes, which locks your tests and page objects into using FluentLenium. But I don’t see that as a big problem, as they will have a dependency on selenium anyway, and it is not a depencency you easily change. Anyhow… back to the jabbering..

    I don’t know what the company I worked for then promised you, but this was my first contact with the selenium framework and automated web tests. If they promised you selenium experts you didn’t get what you asked for.

    The second problem was more related to how the project was run. We didn’t get access to the code, to change the jsp/html to be more testable. We didn’t sit with the developers and testers in the team. This resulted in long turnaround when we had questions, and horrible ugly advanced xpath to find the correct elements. I understand if someone would feel like that it is horrible to maintain.

    The third problem was customer feedback. To be able to deliver what the customer asks for you need to get constructive feedback. This can be hard to give if the customer don’t know what they want. This is a situation we got into during the project. We got feedback that our tests took to long to run and generally sucked, but nothing constructive on what they wanted instead. If we would have been experts on selenium we could have guided you in what you wanted, but this was not the case.

    So that was my 2 cents. Both Peter and I work in the same company now, and I think we both learned that when bringing in outside help we get the best result if they sit with together with the team. But that it is also important to bring in the right people for the job, I think we did a good job given the circumstances, but it obviously wasn’t enough. At least it was a great learning experience 😉

    Reply
  2. Shash

    Hello Peter,

    This is wonderful blog and I was looking for this kind of info on framework. Could you please tell me if you have soruce code for this somwhere I can download? This would save lot of time for me

    Thanks
    Shash

    Reply
  3. ap

    Hi, nice approach. I’m trying to reuse what you have written. It works fine for one test class, but I’m getting the “org.openqa.selenium.remote.SessionNotFoundException: The FirefoxDriver cannot be used after quit() was called.” exception if I try to run different test classes (SeleniumTestCase). It seems the TestScope is not creating new bean instances. Any help would be much appreciated 🙂

    Reply
  4. Samir Khatri

    Thank you for such an enlightening post on spring-selenium testing.
    We are about to start with selenium test for an e-commerce site. Information here would provide solid ground we needed to be on before we start with automate testing.

    Reply
  5. James

    Nice write-up.

    Along with using a fluent interface a good idea is to initialise your forms with default values (I use https://github.com/DiUS/java-faker for the majority of this) and allow them to be overridden when needed. You then make reference to the data using a getter during assertions if you need to check that it has been mirrored back in the ui. This will save you having to figure out values for each of you tests (Things likes names usually dont have any effect on the outcome of a test and its really hard to come up with unique names, save all that work for variables and method names. 😛 ).

    Id also advise to create form objects. Abstracting these out will save you a lot of pain and will allow you over time to extract out common logic. If you do this you test will be more resilient to change. If you make a change to the form you only change it in one place and in the tests that care about the exact field you change.

    A nice to have is to use Cucumber for the docs of the regression tests. It also has some great uses if you make heavy use of tags in that you can create subsets of your suite to run depending on their use and context (eg regression suite, fast/slow tests, work in progress subsets, etc).

    Another thing which gives extra value to your tests is to use them to automatically find where a regression was introduced. If you’re using git you can use GIT bisect along with a single test or a small suite of tests to figure out which commit introduced a regression. (Its like being able to TDD a bug: write test, watch it fail, find regression point, dev, watch it go green, commit).

    By giving a tangible value to the suite you can get more buy in for putting the same effort into them as production code, which in many shops tends to be the biggest reason for bad tests.

    Thanks again for the write up 🙂

    Reply
  6. amacsql

    That was cool stuff and really helped me to understand selenium test automation with page object. Next step could be integrate this stuff with cucumber(BDD approach) which might give a real time user interaction feel for selenium test automation.

    Reply

Leave a comment