Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Testing Web Applications with JBehave, PhantomJS and PageObjects

16.7.2015 | 9 minutes of reading time

Testing web applications is the one thing that could be really really painful. Nevertheless typically there is no way around it when implementing web applications. Luckily the selected tools and applying best practices can help a lot to improve in this area. And hey, JBehave, PhantomJS and PageObjects already sound that cool that testing web applications this way can only be a piece of cake in the end. Well, almost ;-).

Overview

While JBehave and PhantomJS are on the tooling side of this article, PageObjects is a very helpful design approach for testing web applications.

With JBehave the tests as such are implemented in Java and the testcases are specified using Given/When/Then notation. This typically makes the testcases quite readable which helps in the communication between the development team and the subject specialists. Doing the implementation in Java allows the usage of the well known development tools including support for refactoring. Assuming of course that the application itself is implemented in Java as well. Executing tests directly from the IDE during development is definitely a huge advantage here. This is possible with no additional effort and of course due to the Maven-integration of Java the tests can be easily executed on the CI-environment – for example Jenkins – using Maven.

By using PhantomJS the web side of the tests is getting quite lightweight. The tests are executed headless which means that no real browser is started to execute the tests. This makes the tests faster and easier to execute across different environments. Of course this does not help if very specific compatibility issues must be tested for certain browsers, but still then this might be done manually while the majority of tests are automated using PhantomJS. As we will see the integration with JBehave is also working seamlessly.

The concept of PageObjects allows for a very clean design of the test implementation. Basically this is achieved by having one class for each page of the web application that contains all possible interactions with that specific page. Let’s directly jump into the details of this in the following chapter.

Page Objects

Let’s take a look at the following fictitious code-snippet.

1page.loginPage().login("user", "password");
2 
3page.homePage().checkContent();
4page.homePage().gotoPortfolioPage();
5 
6page.portfolioPage().checkAgileExists();
7page.portfolioPage().gotoHomePage();

Looks pretty nice, doesn’t it? As mentioned beforehand the basic concept of PageObjects is to have a Java class that represents one page of the web application. All actions that can be taken on that page are bundled in that “page-class”. On the one hand side this is a quite natural approach once one thinks about it. On the other hand it right away gives an approach to split the complexity of the test implementation into different classes. This is also depicted in the following figure.

Concept of PageObjects

One important part of this is to have one central Pages-class that is a kind of proxy to access all the individual “page-classes”. This way there is no need to add all the individual classes to the classes implementing the individual tests. Furthermore this also allows to keep the state of the pages if this is required.

Often it makes sense to have a common abstract superclass for all the “page-classes” that contains – uh, well – common functionality. This could be own implementations to make screenshots or to check for certain page content or whatever.

A “page-class” could then for example look like this.

1public class HomePage extends AbstractPage {
2 
3    public HomePage(WebDriver webDriver) {
4        super(webDriver);
5    }
6 
7    public void checkContent() {
8        ...
9    }
10 
11    public void gotoPortfolioPage() {
12        ...
13    }
14}

The Pages-class does then nothing else than aggregating all the individual “page-classes” as follows.

1public class Pages {
2 
3    private final WebDriver webDriver;
4 
5    private HomePage homePage;
6    private PortfolioPage portfolioPage;
7 
8    public Pages(WebDriver webDriver) {
9        this.webDriver = webDriver;
10    }
11 
12    public HomePage homePage() {
13        if (homePage == null) {
14            homePage = new HomePage(webDriver);
15        }
16        return homePage;
17    }
18 
19    public PortfolioPage portfolioPage() {
20        if (portfolioPage == null) {
21            portfolioPage = new PortfolioPage(webDriver);
22        }
23        return portfolioPage;
24    }
25}

The usage of the Pages-class in the JBehave implementation is described in the following chapter. There is also not much more to say about PageObjects. So let’s continue with JBehave.

JBehave

Scenarios and Scenario-Implementation

A good starting point to get a general overview on the JBehave concepts could be our previous article on JBehave and its configuration features .

The following figure gives an overview on how tests are implemented using JBehave. The implementation part is on the left-hand side with the Scenario- and Step-classes. While writing the stories in Given/When/Then notation could be done by subject experts in close cooperation with the development team.

JBehave Scenarios

Whenever Given/When/Then notation is used one has to write that tests can then be written by the subject experts. But trust me, I have never seen a project where this has really worked, but still it is probably the best notation currently available … and one can still hope :-).

Let’s take a look at our fictitious test scenario for the homepage. This would be stored under src/main/resources/codecentric/scenarios/homepage/homepage_menu_scenarios.story.


Narrative:
This is just a very simple example for a JBehave scenario. 
In a real project some more precise information could be put 
here to give a context for the test scenarios.
The following scenarios are testing the Main Menu Entries of the 
Homepage.

Scenario: Check Main Menu Entry Portfolio
Given I opened the codecentric homepage
When I clicked on Portfolio 
Then I see the Portfolio page

Scenario: Check Main Menu Entry Career 
Given I opened the codecentric homepage
When I clicked on Career 
Then I see the Career page

As already mentioned, there must be a Scenario-class for each kind of scenario. In this case a HomepageMenuScenarios.java under src/main/java/codecentric/scenarios/homepage. Scenario-classes are typically very easy to write as they are not containing any real test-implementation but they are building the frame to execute the tests.

1package codecentric.scenario.homepage;
2 
3import codecentric.common.CommonScenario;
4import codecentric.step.homepage.MainMenuSteps;
5import org.jbehave.core.steps.InjectableStepsFactory;
6import org.jbehave.core.steps.InstanceStepsFactory;
7import org.junit.Test;
8 
9public class HomepageMenuScenarios extends CommonScenario {
10 
11    @Override 
12    public InjectableStepsFactory stepsFactory() {
13        return new InstanceStepsFactory(configuration(), 
14            new MainMenuSteps(pages), new PortfolioSteps(pages));
15    }
16}

If you looked carefully at the above code for sure you have noticed that we are using a base class here called CommonScenario. This is the place where for example the configuration is defined and also the Pages-object is instantiated. But let’s have another look at the code above. The given method basically defines which step classes are accessible. What a step-class is will be shown a bit later, but basically that is where the real testing-code is implemented. You can add as many step-classes to the stepsFactory-method as needed. Most of the time that will probably be only that step-class required for the given scenarios, but sometimes you need additional step-classes. You will notice when the time comes. Let’s take a look at the CommonScenario-class.

1package dairynet.common;
2 
3import codecentric.pages.Pages;
4import de.codecentric.jbehave.junit.monitoring.JUnitReportingRunner;
5import org.jbehave.core.Embeddable;
6import org.jbehave.core.configuration.Configuration;
7import org.jbehave.core.embedder.Embedder;
8import org.jbehave.core.junit.JUnitStory;
9import org.jbehave.core.reporters.Format;
10import org.jbehave.core.reporters.StoryReporterBuilder;
11import org.jbehave.web.selenium.*;
12import org.junit.Test;
13import org.junit.runner.RunWith;
14import org.openqa.selenium.WebDriver;
15import org.openqa.selenium.phantomjs.PhantomJSDriver;
16import org.openqa.selenium.phantomjs.PhantomJSDriverService;
17import org.openqa.selenium.remote.DesiredCapabilities;
18import java.util.Arrays;
19 
20@RunWith(JUnitReportingRunner.class)
21public abstract class CommonWebScenario extends JUnitStory {
22 
23    private SeleniumContext context = new SeleniumContext();
24    private ContextView contextView = new LocalFrameContextView().sized(500, 100);
25    private WebDriver webDriver = null;
26 
27    protected Pages pages = null;
28 
29    @Override
30    public Configuration configuration() {
31 
32        DesiredCapabilities caps = new DesiredCapabilities();
33        caps.setJavascriptEnabled(true);
34    webDriver = new PhantomJSDriver(caps);
35 
36    pages = new Pages(webDriver);
37    Class<? extends Embeddable> embeddableClass = this.getClass();
38 
39        Configuration configuration = new SeleniumConfiguration()
40            .useSeleniumContext(context)
41            .useStoryLoader(new LoadFromClasspath(embeddableClass))
42            .useStoryReporterBuilder(new StoryReporterBuilder()
43                .withFailureTrace(true)
44                .withFailureTraceCompression(true)
45                .withFormats(Format.HTML, Format.STATS, Format.CONSOLE));
46 
47        return configuration;
48    }
49 
50    @Override
51    public Embedder configuredEmbedder() {
52        final Embedder embedder = super.configuredEmbedder();
53        embedder.useMetaFilters(Arrays.asList("-skip"));
54        return embedder;
55    }
56 
57    @Override
58    @Test
59    public void run() throws Throwable {
60        super.run();
61    }
62}

Here we see our first interaction with PhantomJS. The rest is kind of boilerplate code for JBehave to be able to run stories from Maven and also as JUnit tests directly from the used IDE. If you are interested in more details on configuring JBehave-scenarios, I would again point out one of our previous blog posts .

Steps and Step-Implementation

The Step-classes are the place where the real testing-functionality is implemented, using the PageObject-classes.
Basically there is a simple mapping from the Given/When/Then clauses in der scenario-files to corresponding annotations in the Step-classes. We have seen beforehand that the Step-classes are loaded by the Scenario-classes and this way they are “found”.

We can also see in the examples that it is possible to introduce variables here which is obviously required to have meaningful and especially reusable Step-classes.

A common mistake is to write @Given(„Given I opened the codecentric homepage“) instead of @Given(„I opened the codecentric homepage“) which leads to rather strange error messages later on when executing the tests.

In the corresponding methods of the Step-classes we are then implementing the required test functionality using our Page-Objects and potentially other classes for checking e.g. database content, etc. There are in principle no limits.

Nevertheless this approach requires discipline and potentially refactorings as we should really try to make our steps reusable and they must work in any Scenario and not only in one! This is a really important point here as only then we will have a set of keywords – or we could call them clauses – that can be used to write new tests without having the need to touch the implementation all the time. Furthermore the proper balance between more concrete and more generic implementatons must be found.

PhantomJS

By using PhantomJS some of the obstacles of we testing vanish. This is especially true for setting up CI environments where certain browsers can be started. Downloading and installing PhantomJS can quickly be done .

With PhantomJS the tests are running in headless mode and most of the time this is a good thing. Whoever has worked with tools like Selenium will feel comfortable here right away. The API offers all the usual functionality required for testing web applications.

If you really need to see what is going on for troubleshooting reasons, you can at any time switch to the ChromeDriver for example.

1// webDriver = new PhantomJSDriver(caps);
2webDriver = new ChromeDriver(caps);

But as it is always possible to take screenshots, there is hardly any need for this. Taking screenshots at key places in the testing can make sense to have the possibility to visually check certain things still. This can be especially useful in the beginning when starting to write new tests.

Summary

Using the right tools and the right software design can improve the quality and maintainability of your test code a lot. Of course there are a lot of possible tools and ways to test web applications. But I could really recommend trying out this approach in a small project maybe to get started.

The tools integrate well into the Java ecosystem and they are documented rather good. In addition a lot of blog articles are dealing with the tools. Using PageObjects as an approach helps in organizing the test code and offers a kind of standard for writing web tests.

Just give it a try :-).

share post

Likes

0

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.