Dependency injection and abstraction.
Every rose has its thorn.
In the unlikely event that you have read all of my posts, you will be aware that I have had a try at dependency injection before but wasn’t happy enough with the results to push them. After looking at it this week I have managed to understand where my difficulties lie and and implement a solution that works. I’m still not really happy with it, but its enough to be able to get going on extending my testing beyond the home page. Much as I have enjoyed messing around with static testing and testing my driver factory itself, I can’t wait to get back to the point of this blog, using the Page Object Model.
The problem:
Right back as I was starting my preparations for DI I made a fateful decision. The key object that is needed throughout our framework is the WebDriver. As I discovered back then, it is perfectly possible to leave Selenium itself to control the WebDriverService by just creating a new WebDriver instance. This is the conventional approach taken by pretty much everyone. I chose instead to create a WebDriverService instance for each test class to cut down on some object creation and speed things up. The problem as far as I can see it, is that if I use a WebDriverManager provider within Guice; then Guice has to build the object graph before a WebDriver has been created. This in turn means that I cannot directly inject a WebDriver.
I’m sure that I will return to looking at this later as I am not entirely happy with my solution, but it works so here goes.
The Plan:
The aim in getting this running now, is to enable me to refactor my tests into a 3 layer abstraction model. I’m not going the whole way to BDD and the gherkin language, but I do want my tests to be understandable to someone who knows the website in question. If a manual tester could not at least perform the navigation required from reading the test class then I have failed in my attempts at abstraction.
- The Test classes – list the tasks to be completed.
This is a high level, English language description of the tasks I am performing. The steps methods present me with a fluent interface so that I typically chain method calls together, only having to directly reference a steps class where I back out of a path I have already travelled.
- The Steps classes – Perform a task.
These are classes that translate a simple activity from test class and break it down into a series of calls to methods in the Page / Block classes. Asserts will usually be at this level. Steps methods will typically return a steps class, either a self reference, or the next step class as a result of a navigation.
- The Page / Block classes – Do or Get.
This is the inner most layer that interacts with the browser through Selenium WebDriver.
A Page is an object to interface with the page presented by a given url. Its methods will interact, both in terms of clicking and entering text / values, and in returning the values of objects on the page.
A Block (not implemented yet) is like a Page, but having switched context to one element within the full page.
The implementation:
As mentioned previously, the goal is to use Guice to inject my dependencies.
- Steps, pages and blocks use constructor injection.
- Test classes use field injection.
There is rather too much going on to describe how I achieved that this week in this post, so today I will talk through the abstraction levels, next week I’ll explain how I set up the injection and why. HINT: it is not as clean as I would like as you will probably notice here.
Tests
package tech.alexontest.poftutor; import com.google.inject.Inject; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import tech.alexontest.poftutor.infrastructure.AbstractTest; import tech.alexontest.poftutor.steps.HomePageSteps; @Tag("Content") class HomePageTests extends AbstractTest { @Inject private HomePageSteps homePageSteps; @BeforeAll void setupHomepage() { homePageSteps.loadHomePage(); } @Test @DisplayName("Reported URL is correct") void correctUrlIsReported() { homePageSteps.assertThatReportedUrlIsCorrect(); } @Test @DisplayName("Tab page title is correct") void correctTitleIsDisplayedOnTheBrowserTab() { homePageSteps.assertThatBrowserTabPageTitleIsCorrect(); } @Test @DisplayName("The correct title is reported") void titleIsCorrect() { homePageSteps.assertThatTitleIsCorrect(); } @Test @DisplayName("5 Widgets appear on the Homepage") void pageContainsFiveWidgets() { homePageSteps.assertThatPageContainsFiveWidgets(); } @Test @DisplayName("No more than 5 posts are linked on the Homepage") void pageContainsUpToFiveArticles() { homePageSteps.assertThatPageContainsUpToFiveArticles(); } }
Edit: 3 October 2018
Thank you to Aleksandar Stefanović for pointing out that something doesn’t look quite right about this class above. He is quite right that
@BeforeAll
must be static unless annotated with@TestInstance(TestInstance.Lifecycle.PER_CLASS)
.In this case I actually had the annotation attached to the AbstractTest class from which this class inherits.
You can see this more clearly in the next post where I describe the injection implementation. I did say that I wasn’t very happy with the code at this point!
At this point they are very simple as I have not yet navigated from the Homepage. Given that each test is a very simple task, they are each a single line.
Now I have my DI framework set up, it is easy for me to start pushing into the website over the next few weeks and building longer test scripts. The overhead of the time taken to start up a WebDriver and navigate to the page is why I am using a class that performs all of these tests in a single driver without navigation after the initial set-up.
package tech.alexontest.poftutor; import com.google.inject.Inject; import org.junit.jupiter.api.Tag; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import tech.alexontest.poftutor.infrastructure.AbstractTest; import tech.alexontest.poftutor.steps.HomePageSteps; @Tag("Navigation") class NavigationTests extends AbstractTest { @Inject private HomePageSteps homePageSteps; @ParameterizedTest(name = "{0} returns 'https://alexanderontesting.com' as expected.") @CsvSource({ "https://alexanderontesting.com/", //"https://alexanderontesting.com/", //TODO fix http redirection w/o breaking others "https://alexanderontesting.com/", "https://alexanderontesting.com/", "https://alexontest.tech/", "http://alexontest.tech/", "https://www.alexontest.tech/", "http://www.alexontest.tech/", }) void allUrlsLeadToHere(final String urlToTest) { homePageSteps .attemptToLoadHomePageFromUrl(urlToTest) .assertThatReportedUrlIsCorrect(); } }
In this case I am still using a single driver, but navigating to a new url for each test. Here we can see the fluent interface in action. I could easily have incorporated these into a single method call, but this is easier to read what the test is doing. I prefer readability above all, hence long descriptive method names and fields / variables. Note that I have the test data that changes here in the test (the navigation url) but the test data that is a property of the homepage (the reported url) stored in the homepage class. This is to ensure that I have it in one place only (the DRY principle) so if I ever change it, there is only a single change to be made. I did consider setting the reported url as a constant and referencing it as a parameter in the method call to make it clear that all navigation points report the same url but this will do.
Yes, yes I know I really should fix the redirection problem, but it has defeated my first couple of attempts, and I prefer to spend my limited time working on solving problems with the testing. It’s a shame I can’t just submit a bug report and wait for the dev team to fix it for me!
Steps
As of yet I still haven’t gone beyond the homepage, so just the one steps class.
package tech.alexontest.poftutor.steps; import com.google.inject.Inject; import org.openqa.selenium.WebDriver; import tech.alexontest.poftutor.infrastructure.configuration.TestConfiguration; import tech.alexontest.poftutor.infrastructure.driver.WebDriverManager; import tech.alexontest.poftutor.pages.HomePage; import static org.assertj.core.api.Assertions.assertThat; import static tech.alexontest.poftutor.Constants.MAX_POSTS_PER_LISTING_PAGE; import static tech.alexontest.poftutor.Constants.WIDGETS_PER_PAGE; public class HomePageSteps { private final HomePage homePage; private final TestConfiguration testConfiguration; private final WebDriver webDriver; @Inject public HomePageSteps(final HomePage homePage, final TestConfiguration testConfiguration, final WebDriverManager driverManager) { this.homePage = homePage; this.testConfiguration = testConfiguration; webDriver = driverManager.getDriver(); } public void loadHomePage() { webDriver.navigate().to(testConfiguration.getHomePageUrl()); } public HomePageSteps attemptToLoadHomePageFromUrl(final String url) { webDriver.navigate().to(url); return this; } public void assertThatReportedUrlIsCorrect() { assertThat(webDriver.getCurrentUrl()) .as("Reported URL is not as expected") .isEqualTo(homePage.getURL()); } public void assertThatBrowserTabPageTitleIsCorrect() { assertThat(webDriver.getTitle()) .as("Reported browser tab page title is not as expected") .isEqualTo(homePage.getBrowserTabPageTitle()); } public void assertThatTitleIsCorrect() { assertThat(homePage.getTitle()) .isEqualToIgnoringCase("Alexander on Testing"); } public void assertThatPageContainsUpToFiveArticles() { assertThat(homePage.getArticles()) .size() .isLessThanOrEqualTo(MAX_POSTS_PER_LISTING_PAGE); } public void assertThatPageContainsFiveWidgets() { assertThat(homePage.getWidgets()) .size() .isEqualTo(WIDGETS_PER_PAGE); } }
Note that, as yet, only a single method leads to any follow on so has a return method set. I am a little conflicted between the clean code principle of only setting return methods when they are used, and the idea that every method should return a self reference unless it navigates away from this page. By having every steps method return a valid steps class where one exists, anyone writing a test class gets auto-complete options in the IDE.
Page
package tech.alexontest.poftutor.pages; import org.openqa.selenium.WebDriver; import tech.alexontest.poftutor.infrastructure.driver.WebDriverManager; abstract class AbstractPage implements Page { private final WebDriver driver; AbstractPage(final WebDriverManager webDriverManager) { this.driver = webDriverManager.getDriver(); } protected WebDriver getDriver() { return driver; } }
This is where I am not entirely happy about my dependency injection as I am having to inject the WebDriverManager instead of the driver that I really need. This appears to work well enough so I guess time will tell if there are any major shortcomings.
package tech.alexontest.poftutor.pages; import com.google.inject.ImplementedBy; import org.openqa.selenium.WebElement; import java.util.List; @ImplementedBy(HomePageDesktop.class) public interface HomePage extends Page { String getTitle(); List<WebElement> getWidgets(); List<WebElement> getArticles(); }
A few changes here however demonstrate the power of the Inversion of Control pattern. My first working attempt injected the WebDriverManager into the Page implementation, but the constructor of the AbstractPage was still requiring the WebDriver itself. I have changed this, and renamed my implementation class and only these three classes have any changes to push. All any other class has to do is to ask for a HomePage Object. As long as the interface doesn’t change, implementation changes will not affect the rest of the code.
package tech.alexontest.poftutor.pages; import com.google.inject.Inject; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; import org.openqa.selenium.support.PageFactory; import tech.alexontest.poftutor.infrastructure.driver.WebDriverManager; import java.util.List; public final class HomePageDesktop extends AbstractPage implements HomePage { @FindBy(css = ".site-title") private WebElement title; @FindBy(css = ".widget") private List<WebElement> widgets; @FindBy(css = ".post-content") private List<WebElement> articles; @Inject public HomePageDesktop(final WebDriverManager webDriverManager) { super(webDriverManager); PageFactory.initElements(getDriver(), this); } @Override public String getBrowserTabPageTitle() { return "Alexander on Testing – Adventures in Software Testing"; } @Override public String getURL() { return "https://alexanderontesting.com/"; } @Override public String getTitle() { return title.getText(); } @Override public List<WebElement> getWidgets() { return widgets; } @Override public List<WebElement> getArticles() { return articles; } }
Conclusion
And there you have it. I’ll walk through how I set up the dependency injection itself next week then I am excited to be able to get on with writing some more complex tests now that my framework is ready and there are plenty of posts to navigate around and test. I guess the fact that it has taken me 5 months to get to this stage shows why many people prefer to pick up a pre-built framework.
Other news this week:
- I notice from my analytics that a couple of people have pulled and run my test code. (Either that or some crazy people just love loading my Homepage!) This is not a problem for now at least as this is well within my hosting plan and it would be really good to hear if you found this useful. I would love some feedback to alexander at alexanderontesting.com or sign up and leave some comments. What if anything did you find useful, and do you have any suggestions for improvement or questions. This project is really about my own learning, but if it helps others so much the better.
- One other change: As many others, I am getting rather alarmed at the extent to which many organisations seem to be developing solely for Google Chrome. Given this, I think its about time that I switched my default browser. As such I have set it to Firefox for now as the second most popular desktop browser globally although it is far behind Chrome (12% compared to 67%.) Internet Explorer is still hanging around at (7%) but so slow to automate I hate it. I’m still working on acquiring a Mac for Safari (6%) and, much as I love the speed of testing on Edge, its 4% share has barely changed in the last year. My aim however is to ensure that my tests run on all browsers and hopefully verify that my website does too.
Progress made:
- Finally managed to get Dependency Injection firing using Google Guice.
- Separated the logic out into 3 abstraction layers of Tests – Steps – Pages.
- Test browser is configured in a configuration file and set to Forefox.
Lessons learnt:
- Sometimes solving a problem will take a few attempts.
- Going live with something that works even if you aren’t entirely happy with it is sometimes required.
- Sometimes a week of code take much longer to explain.
Next week I’ll be explaining the process of getting the Dependency injection running.
One thought on “Dependency injection and abstraction.”
Comments are closed.