Selenide vs Fluentlenium #2 - Page objects
Creating test framework is quite long and complicated process in which we have to define the code structure. It is important because the better it is defined the easier maintaining will be. To make this process easier and code even more transparent we can use some features of the Selenide or Fluentlenium. In this article I will describe on example when those frameworks come to help us in the whole process of tests development and maintenance.
Page object pattern is probably the most popular pattern used in Selenium based frameworks. It gives possibility to create separate classes which represents the views of the application and put there methods represents the actions possible to perform by the user on that very view. In this article I will create simple test framework with use of Page Object pattern in Fluentlenium and Selenide. The test I will create will just navigate to the testcraftsmanship.com page, scroll to Articles section, open details of one of the articles and validate if the title and content is correct. In general with use of Page Object patter to cover that scenario we need to create two classes which represents views. The first one will represent the main page and that page contains two components/subpages: menu section and section with articles. Representation of the class that represents the Menu can looks for example like that:
package com.testcraftsmanship.selenide.page.sections; import com.codeborne.selenide.SelenideElement; import com.testcraftsmanship.selenide.page.MainPage; import org.openqa.selenium.support.FindBy; import static com.codeborne.selenide.Selenide.page; public class MenuSection { @FindBy(css = ".menu-about-me") private SelenideElement aboutMeButton; @FindBy(css = ".menu-home") private SelenideElement homeButton; @FindBy(css = ".studies") private SelenideElement studiesButton; @FindBy(css = "a[href*='articles']") private SelenideElement articlesButton; @FindBy(css = ".contact") private SelenideElement contactButton; public MainPage openArticlesSection() { articlesButton.click(); return page(MainPage.class); } }
I have added only one method which is required by the test but I added all elements
to give you better understanding what should that class contains. The next required
class is describing Articles section and can looks like that:
package com.testcraftsmanship.selenide.page.sections; import com.codeborne.selenide.ElementsCollection; import com.codeborne.selenide.SelenideElement; import com.testcraftsmanship.selenide.page.ArticlePage; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.support.FindBy; import static com.codeborne.selenide.Selenide.page; public class ArticlesSection { @FindBy(css = "div.post") private ElementsCollection articles; private static final String TITLE_CSS_SELECTOR = "h2.post-title"; private static final String AUTHOR_CSS_SELECTOR = "div.post-meta > a"; private static final String CONTENT_CSS_SELECTOR = ".post-entry"; private static final String LINK_CSS_SELECTOR = ".more-link"; public ArticlePage openArticleByTitleAndAuthor(String title, String author) { getMatchingArticleElement(title, author) .find(LINK_CSS_SELECTOR) .click(); return page(ArticlePage.class); } public String getArticleContent(String title, String author) { return getMatchingArticleElement(title, author) .find(CONTENT_CSS_SELECTOR) .text(); } private SelenideElement getMatchingArticleElement(String title, String author) { return articles.stream() .filter(el -> el.find(TITLE_CSS_SELECTOR).getText().toLowerCase().equals(title.toLowerCase()) && el.find(AUTHOR_CSS_SELECTOR).getText().toLowerCase().equals(author.toLowerCase())) .findFirst() .orElseThrow(() -> new NoSuchElementException("No element with title: " + title)); } }
Unfortunately I haven't found better solution for working with collection of components/subpages
that's why openArticleByTitleAndAuthor method is a little bit complicated. The
method simply gets all articles, filters them by title and author and after that
clicks on link of the first found article to open its detailed view. Selectors for
all subelements of the article has to be defined in that class.
When those two classes are ready we can create class which represents Main page.
package com.testcraftsmanship.selenide.page; import com.testcraftsmanship.selenide.page.sections.ArticlesSection; import com.testcraftsmanship.selenide.page.sections.MenuSection; import static com.codeborne.selenide.Selenide.page; public class MainPage { private MenuSection menuSection = page(MenuSection.class); private ArticlesSection articlesSection = page(ArticlesSection.class); public MenuSection fromMenuSection() { return menuSection; } public ArticlesSection fromArticlesSection() { return articlesSection; } }
This class contains those two created before - it should be cristal clear as
menu and articles section are the part of the Main page.
The last page we need represents the details view of the article. As you
can see below it has its own logic but also contains MenuSection.
package com.testcraftsmanship.selenide.page; import com.codeborne.selenide.SelenideElement; import com.testcraftsmanship.selenide.page.sections.MenuSection; import org.openqa.selenium.support.FindBy; import static com.codeborne.selenide.Selenide.page; public class ArticlePage { private MenuSection menuSection = page(MenuSection.class); @FindBy(css = "h1.post-title") private SelenideElement postTitle; @FindBy(css = "a[href*='#about-me']") private SelenideElement author; @FindBy(css = ".post-entry") private SelenideElement postContent; public MenuSection fromMenuSection() { return menuSection; } public SelenideElement getPostTitle(){ return postTitle; } public SelenideElement getPostContent(){ return postContent; } }
When we have all the puzzles ready we can create the test with use of created framework.
package com.testcraftsmanship.selenide; import com.codeborne.selenide.Configuration; import com.testcraftsmanship.selenide.page.ArticlePage; import com.testcraftsmanship.selenide.page.MainPage; import com.testcraftsmanship.selenide.page.sections.ArticlesSection; import org.junit.Before; import org.junit.Test; import static com.codeborne.selenide.Condition.text; import static com.codeborne.selenide.Selenide.open; public class QuickStartTest { @Before public void setUp() { Configuration.browser = "chrome"; } @Test public void articleDetailsShouldContainsCorrectData() { final String postTitle = "DEALING WITH ASSERTIONS IN SELENIUM TESTS"; final String author = "Grzegorz Szczutkowski"; ArticlesSection articlesSection = open("https://testcraftsmanship.com", MainPage.class) .fromMenuSection() .openArticlesSection() .fromArticlesSection(); String articleShortContent = articlesSection.getArticleContent(postTitle, author); ArticlePage articlePage = articlesSection.openArticleByTitleAndAuthor(postTitle, author); articlePage .getPostTitle() .shouldHave(text(postTitle)); articlePage .getPostContent() .shouldHave(text(articleShortContent)); } }
Now let's create the framework in Fluentlenium
which allows us to create the same test scenario. In
Fluentlenium we are able to create components so in our framework
the first component will be the single short article component from the Articles section on Main page -
the one that contains title, author, creation date and short description. The
implementation of this component can looks like:
package com.testcraftsmanship.selenide; import com.codeborne.selenide.Configuration; import com.testcraftsmanship.selenide.page.ArticlePage; import com.testcraftsmanship.selenide.page.MainPage; import com.testcraftsmanship.selenide.page.sections.ArticlesSection; import org.junit.Before; import org.junit.Test; import static com.codeborne.selenide.Condition.text; import static com.codeborne.selenide.Selenide.open; public class QuickStartTest { @Before public void setUp() { Configuration.browser = "chrome"; } @Test public void articleDetailsShouldContainsCorrectData() { final String postTitle = "DEALING WITH ASSERTIONS IN SELENIUM TESTS"; final String author = "Grzegorz Szczutkowski"; ArticlesSection articlesSection = open("https://testcraftsmanship.com", MainPage.class) .fromMenuSection() .openArticlesSection() .fromArticlesSection(); String articleShortContent = articlesSection.getArticleContent(postTitle, author); ArticlePage articlePage = articlesSection.openArticleByTitleAndAuthor(postTitle, author); articlePage .getPostTitle() .shouldHave(text(postTitle)); articlePage .getPostContent() .shouldHave(text(articleShortContent)); } }
As you can see components are created on similar way as pages but they extends
FluentWebElement. In the class we defined the all elements we need in our tests with
their selectors and defined actions can be done on the very article - we can get the
title, author and content but also we can click on link to open full article.
The next component we need is the one from main page hat contains all the articles. This one will be called ArticlesSection so the same as for Selenide framework.
package com.testcraftsmanship.fluentlenium.page.sections; import org.fluentlenium.core.FluentControl; import org.fluentlenium.core.components.ComponentInstantiator; import org.fluentlenium.core.domain.FluentList; import org.fluentlenium.core.domain.FluentWebElement; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; public class ArticlesSection extends FluentWebElement { @FindBy(css = "div.post") private FluentList<ArticleComponent> articles; public ArticlesSection(WebElement element, FluentControl control, ComponentInstantiator instantiator) { super(element, control, instantiator); } public ArticleComponent getArticleByTitleAndAuthor(String title, String author) { return articles.stream() .filter(art -> art.title().toLowerCase().equals(title.toLowerCase()) && art.author().toLowerCase().equals(author.toLowerCase())) .findFirst() .orElseThrow(() -> new NoSuchElementException("There is no article with title " + title)); } }
This part differs the most from Selenide.
In Selenide we had to define selectors
for all subelements of the article and to perform any action (e.g. getting content) on
article we had to first get all elements and then filter interesting element. In Fluentlenium
we can get the full interesting component with all its elements and after that perform
those actions on the very component.
The next component we need is MenuSection which can look as shown below:
package com.testcraftsmanship.fluentlenium.page.sections; import com.testcraftsmanship.fluentlenium.page.MainPage; import org.fluentlenium.core.FluentControl; import org.fluentlenium.core.components.ComponentInstantiator; import org.fluentlenium.core.domain.FluentWebElement; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; public class MenuSection extends FluentWebElement { @FindBy(css = ".menu-about-me") private FluentWebElement aboutMeButton; @FindBy(css = ".menu-home") private FluentWebElement homeButton; @FindBy(css = ".studies") private FluentWebElement studiesButton; @FindBy(css = "a[href*='articles']") private FluentWebElement articlesButton; @FindBy(css = ".contact") private FluentWebElement contactButton; public MenuSection(WebElement element, FluentControl control, ComponentInstantiator instantiator) { super(element, control, instantiator); } public MainPage openArticlesSection() { articlesButton.click(); return newInstance(MainPage.class); } }
Page object that represents the Main page can be defined as shown below:
package com.testcraftsmanship.fluentlenium.page; import com.testcraftsmanship.fluentlenium.page.sections.ArticlesSection; import com.testcraftsmanship.fluentlenium.page.sections.MenuSection; import org.fluentlenium.core.FluentPage; import org.fluentlenium.core.annotation.PageUrl; import org.openqa.selenium.support.FindBy; @PageUrl("https://testcraftsmanship.com") public class MainPage extends FluentPage { @FindBy(css = ".navbar") private MenuSection menuSection; @FindBy(css = "#articles") private ArticlesSection articlesSection; public MenuSection fromMenuSection() { return menuSection; } public ArticlesSection fromArticlesSection() { return articlesSection; } }
Here we can see the next difference from Selenide that every component we want to
include into the class has to contain its selector defined in @FindBy annotation. As
those components are lazy initialized they do not need to be present while declaring
the MainPage object but while performing the action on the very component.
The last class we need is the one which defines full article view. Lets call it the same as in Selenide framework and it can look as shown below:
package com.testcraftsmanship.fluentlenium.page; import com.testcraftsmanship.fluentlenium.page.sections.MenuSection; import org.fluentlenium.core.FluentPage; import org.fluentlenium.core.domain.FluentWebElement; import org.openqa.selenium.support.FindBy; public class ArticlePage extends FluentPage { @FindBy(css = ".navbar") private MenuSection menuSection; @FindBy(css = "h1.post-title") private FluentWebElement postTitle; @FindBy(css = ".post-entry") private FluentWebElement postContent; public MenuSection fromMenuSection() { return menuSection; } public String postTitle(){ return postTitle.text(); } public String postContent(){ return postContent.text(); } public FluentWebElement getPostTitle(){ return postTitle; } public FluentWebElement getPostContent(){ return postContent; } }
As we have all required elements ready we can create the same test scenario as for
Selenide. The scenario can look for example as shown below:
package com.testcraftsmanship.fluentlenium; import com.testcraftsmanship.fluentlenium.page.ArticlePage; import com.testcraftsmanship.fluentlenium.page.MainPage; import com.testcraftsmanship.fluentlenium.page.sections.ArticleComponent; import org.fluentlenium.adapter.junit.FluentTest; import org.fluentlenium.core.annotation.Page; import org.junit.BeforeClass; import org.junit.Test; import static org.fluentlenium.assertj.FluentLeniumAssertions.assertThat; public class QuickStartTest extends FluentTest { @Page MainPage mainPage; @BeforeClass public static void requiredSetUp() { String chromeDriverPath = QuickStartTest.class.getResource("/chromedriver.exe").getPath(); System.setProperty("webdriver.chrome.driver", chromeDriverPath); } @Test public void articleDetailsShouldContainsCorrectData() { final String postTitle = "DEALING WITH ASSERTIONS IN SELENIUM TESTS"; final String author = "Grzegorz Szczutkowski"; ArticleComponent shortArticle = openMainPage() .fromMenuSection() .openArticlesSection() .fromArticlesSection() .getArticleByTitleAndAuthor(postTitle, author); String articleShortContent = shortArticle.content(); ArticlePage articlePage = shortArticle.open(); assertThat(articlePage.getPostTitle()).hasText(postTitle); assertThat(articlePage.getPostContent()).hasText(articleShortContent); } private MainPage openMainPage() { goTo(mainPage); await().until($(".page-loader")).not().displayed(); return newInstance(MainPage.class); } }
Base on that you can compare Selenide,
Fluentlenium and the way you can create the
frameworks with use those two. The article was focused on creating frameworks with use
of Page Object pattern and project classes structure. From my point of view
Fluentlenium approach is a little bit more
clear and intuitive because of introducing Components concept. I can not say that
it is better because it is more about the preferences and probably you should decide
what suits you more. Before you take the final decision I recommend to read the last
article about the comparison of those two frameworks which will be focused on the
timing issues and way of solving them in those two frameworks - the article will
be available soon.