Sunday, August 2, 2015

Java 8 impact on test automation framework design - Part 2

In the first part I've shown you how to prettify UI tests using Java 8 interfaces. This time, we'll take a look at a more complicated example.

Well, you may know that there're 2 common ways of accessing web controls via WebDriver:
  1. @FindBy + WebElement -> automatic lookup with PageFactory.initElements.
  2. By -> delayed lookup with driver.findElement or WebDriverWait + ExpectedConditions.
Personally I prefer second option, as it gives better flexibility, while working with complicated JS-based websites, when we always need to wait for something. But on the other hand, By class seems a bit non-obvious, plus there's no factory implemented for this case yet. Well, actually in the first part we did created a custom factory, so we could say that this problem has gone. But besides that, it'd be nice to see some similar elements' definition style, as it was implemented for pure WebElement.

In this article I'll show you how to create custom typified elements and a generic initializer, similar to mentioned above initElements.

We'll modify some part of code from one of my previous articles, as the idea remains the same: creating a custom HTML annotation and HTMLElement class. But this time we'll also implement some more specific elements, like Button, TextInput, Label, etc., mostly as it was done in Html Elements framework.

Let's see HTMLElement (aka base element) first. It won't be a full listing, only some key moments:
As we're going to create more specific elements, it's important to pass WebDriver instance to our base element's constructor for further usage in a combination with WebDriverWait. To be honest, there're lots of ExpectedConditions we may use for element's locating, but for educational purposes we'll look only at the most popular: visibilityOfElementLocated, presenceOfElementLocated and elementToBeClickable. All these conditions could be described via the following function:
This function was applied in a generic waitUntil method, so that we could pass any of listed above ExpectedConditions as a parameter. Now we're ready to create some more specific elements, e.g. TextInput:
As you can see, it extends HTMLElement. Now we can use waitUntil method to locate only those inputs which are clickable. Besides that, we've defined a custom logic: clear input and type some text. Note that element's locator reference is physically located in a super class.

Let's assume that we've already created a set of specific elements. So how could we initialize them? If you look at PageFactory sources, you'll notice some dark reflection magic. It'll take you some time to figure out how it's implemented. But I'll show you an alternative, even more generic and darker way of elements' initialization. It'll be still reflection-based, but with a help of Java 8 features we'll see how it could be implemented within single interface.

To make this experiment more realistic, we'll create some other type of element, so that we could see that our elements' supplier is not hardly dependent from a single type and is definitely generic. So what type of element would it be? Have you ever heard something about SikuliX? It's an OCR tool, which may help us to resolve some complicated automation tasks, which are impossible with Selenium. So before starting with elements' initializer, I'll create a model for SikuliDriver, ScreenElement and its ImageElement implementation. Well, hope some time in the future I'll have enough capacity to implement a full functional approach to make SikuliX closer to WebDriver interface. But for now it'll be just a mock.

Here's a draft implementation, which will be further mocked in test:
There won't be any real clicks or text typing, but we need to know that element has been successfully initialized and we could perform some basic actions.

Well, our alternative model is ready, so now let's add WebDriver and SikuliDriver(mock) into BaseTest class.
Note that well-known initialization / quiting staff was skipped, but you can find a full source later on GitHub. Mockito library was used for mocking, so don't forget to add appropriate dependency into root pom.xml:
And now it's time for something very special. Welcome, our magic interface - ElementsSupplier. I'll try to explain everything within the following listing, as there's a complicated combination of reflection, streams, lambdas and default methods.
Let's start with the end. As you may noticed from HTMLElement and ImageElement constructors' signature, both receive specific drivers as a fist argument, which are needed for further elements locating:
We also have 2 custom annotations - HTML and Image, which values need to parsed and supplied to appropriate elements' constructors side by side with mentioned above drivers. It's a bit tricky moment. In case of a single element's type, we know exactly which annotation to parse, which driver to use and which constructor to call. But our case is more generic. We don't know exactly how many elements' types, drivers and annotations are there. So we can't predict which constructor to call. Here's our first requirement: a class which implements ElementsSupplier interface must provide a list of supported drivers and annotations:
In case or drivers, we may want a Stream of their instances for further passing to matching constructors. In case of annotations, we just need their types to detect if particular instance variable contains one of supported items. Now let's take a look at our BasePage class, which implements ElementsSupplier:
As you can see, we've overridden both abstract methods to provide WebDriver and SikuliDriver instances, as well as HTML and Image annotation types. Now our interface knows a search direction (annotations) and first constructors' arguments (drivers' instances).

We can also see that BasePage default constructor explicitly calls default initElements method, passing this as a parameter. You may wonder, what does this mean in such context? It's a reference on a top-level PageObject, which we have triggered to be initialized. So we ask our interface to initialize all the custom fields within particular class and its super-classes.

Now let's take a look at common algorithm for elements' initialization:
  1. First we need to loop through each declared field of a current PageObject class and its super-classes, until we reach a base Object.class. We can do that with Stream.iterate API. But with one important note. Pure java implementation doesn't support any good exit criteria except setting limit operation. By default we don't know a number of super-classes we want iterate, so the only valid condition for us will be !currentClass.equals(Object.class). Fortunately, there's a great Streams extension library com.codepoetics.protonpack.StreamUtils, which allows us to set appropriate Predicate to break an infinite loop, when condition is met (takeWhile API).
  2. Next we need to loop through all declared class fields and find out, if any of supported annotation types is present.
  3. If anything found, we retrieve annotation by its type and call specialized initElement method for further field initialization.
  4. initElement itself could be splitted into several logical parts. First of all, we need to retrieve all annotation values. It's a bit tricky part, as getDeclaredMethods() API doesn't guarantee to return an ordered list of methods (how they were declared in a class). But order is very important while passing arguments to appropriate constructor. That's why we are using custom methods' comparator (by name), which meets our order requirements. But anyway you could always override default methodsComparator() with your own custom logic.
  5. These are annotations' arguments, but what's about drivers? Our constructors require particular driver instance as a first parameter. Here's the other tricky moment. Both drivers are of generic interface type. And there's no easy way to guess which exact type is assigned to particular object. That's why we have to loop through all supported drivers, insert one to the beginning of annotations' arguments list and pass it deeper to createInstance method.
  6. createInstance uses common java reflection API to intialize our custom elements with provided arguments list. As I've mentioned above, there's no easy way to detect assigned interface type, so we additionally try to check whether WebDriver or SiluliDriver types are assignable to provided arguments. If yes, we return more specific type to be able to find matching constructor. In case of any exception, we return empty Optional. It means that there's no matching constructor found for particular combination of a driver / annotation arguments, and we should try another driver as a first parameter.
  7. The final step is to check if any object instance was created. In a positive case we make field accessible and put a newly initialized reference inside.
That's it. Now we can make sure that all the fields are initialized. There's only 1 note left. As SikuliDriver is mocked, we should also mock ScreenElements to check if our approach works. It could be done somewhere in @BeforeMethod.
Let's declare the same items e.g. in HomePage:
And add appropriate call into test case:
Well, there's no any valid logic in uploadFile call. The only purpose of this is to see a working WebDriver test with appropriate SikuliX console log messages:


That's all. You can find sources as usual on GitHub.

No comments:

Post a Comment