Well, it took me a bit longer than I had expected to resolve all the urgent tasks. But finally I'm back and ready to share some new material with you.
In this article I'd like to describe some fresh thoughts about web automation framework design. I've been playing with different design approaches for several years. Primary goals were: increasing system tests' readability and reducing time on their support.
I believe that any good test should be written in terms of
DSL, so that anyone could understand its context and purpose. As normally we're running tests via CI servers, it's important to reflect all the steps performed during execution in test results report. You can achieve this goal via
AOP and custom annotation to collect everything and inject appropriate info into report. On the other hand, you can use some existing solution like
Allure Test Report.
Well, this is all about test steps. But what's about verifications? Normally, we're using
asserts to compare actual and expected result. When we're talking about UI tests, there could be much more than just a single verification used. Would be nice to see them all in test results report as well, right? Welcome, our first technical blocker. We can't annotate asserts inside test method's body. So the only way to workaround this is to create an assertions wrapper or custom matchers.
Wrapper implementation is a bit out of context of a common inheritance model. Let's assume that we have some
BaseTest class, which is intended to control tests execution flow and some internal staff preparation. As you may know, multiple inheritance is
mostly impossible in Java (I'll describe why
mostly below). It means that our assertions wrapper should be transformed into utility class with static methods. Is it good or bad? There's no exact answer. So I'll leave it for your own analysis.
Matchers - seems like a better solution. But how much time should we waste on their preparation, customization and support? Depends on...
Anyway, I'd like to show you a third way, which is about
'inheritence mostly impossible'. Well, in Java 8 there was introduced a new concept -
default interface methods. To get better understanding of what it is, I'd recommend to read
Java 8 in Action book. Basically, recent interfaces allow us to create methods with bodies. You may wonder what do we need it for? One of potential purposes is to extend existing functionality without making outstanding impact on entire project(-s). As you may know from previous Java versions, when class is connected with interface, it agrees to implement all declared methods. Let's imaging that you're developing some popular library and one day you decided to extend an existing interface with some new method's definition. When you publish an updated version of your library, you may wonder how much angry emails would you receive. The reason is that users' code may fail to compile until they implement your new addition. Imagine if there were lots of entry points, where this interface was defined. Potential impact could be enormous. So how new Java interfaces could help? Well, first of all, default method doesn't require to be overridden. Now you can safely add some extended APIs directly inside interfaces without any impact on related classes. Sounds cool, isn't it? But what's about inheritance? Keeping in mind that default method looks like a common one except some minor syntax differences, plus a fact that a single class may implement any amount of interfaces, we may guess that this opens us a direct way to multiple inheritance. Wow, that's awesome! Let's see how it may help us with our automation routine.
As I've mentioned above, it would be great, besides common steps, to print all the verification staff into test results report as well. We'll start with some preparations first. To avoid re-inventing the wheel,
Allure will be used as a code base for steps definition and printing. It'll be a multi-module
maven project to achieve better domain part separation from framework core. In your root
pom.xml you should add
reporting section with
allure-maven-plugin. Once it's done, just add 2 modules to your root:
core /
domain. Your
pom.xml should now look something similar:
<pre class="brush: xml"></pre>
Let's create some common abstraction layer in
core module. It'll be a
BasePage and
BaseTest classes. We'll leave them blank for a while and continue with
domain module.
Assuming that you're already familiar with
PageObject pattern, we'll need to create a template for some sample test scenario. Let's say we're going to check Google account authorization flow. To achieve this goal we need at least 2 pages:
Login and
Home. Keeping in mind that all the steps should be printed directly into report, we'll use appropriate
@Step annotation from
Allure framework:
As you can see, nothing specific. Just a simple authorization flow with username verification. Well, to resolve missing dependencies we should update
domain module's
pom.xml.
Note that
Allure requires including
AspectJ dependencies to perform steps interception in runtime. As
TestNG was chosen as a unit framework, we had to add appropriate
Allure adaptor, which implements a special listener for collecting necessary test data.
Finally, we can create a simple test using provided above steps.
Pretty straightforward script, isn't it? You may just wonder about
loadUrl and
homePage methods (by the way, it was first mentioned in
LoginPage class). But let's keep an intrigue for a while.
So our main goal is to annotate
assertEquals with
@Step somehow. Besides that, another logical blocker occurs: URL loading action is something that happens mostly only first time, when browser is opened. So logically first navigation step doesn't relate to any page or application itself. In such case, where should we put this API? In
core module? But how we'll return
LoginPage instance then, if framework logically and physically is a completely independent unit, which shouldn't be related to any domain at all? So
domain module then, right? Ok, but again
where should we put it? Our test class already extends
BaseTest. It means that we can't inherit anything else.
And a headshot -
PageFactory. If you have ever worked with
Selenium, you may know that there's a special factory class, which is intended for
PageObjects +
WebElements initialization. Well, and what if I don't use
WebElements? What if use
By locators? Where's my
By factory? Someone may say: you don't need a factory, just use common class initialization technique. Ok, but where should I store getters for my
PageObjects then? Ah, you're saying to create my own factory now? Behind the scene, I'm always wondering, why should I call such low level API directly in tests? Why should I save intermediate page objects state in variables to verify something or just break the chain for some other actions? Maybe I'm a bit idealistic, but I've been looking for some good design approach for a long time to make tests fancy as much as possible, to completely remove all the low level staff from highest abstraction layer. And now... now I can say that I found some technical approach to achieve this goal. As you may guess that's all about
default interface methods.
Let's start with some light scenario -
verification. Everything we need is to create an interface with a simple default method to verify 2 String values - expected / actual result.
As you can see, there's no magic at all. Common interface style, common method's signature, except
default keyword. This basically means that a class, which implements above interface, may call
verifyTextEquals method directly, like in case if this method was a part of it. Isn't it cool? The other big advantage is that we shouldn't necessarily override it. But we still have such opportunity, if really needed.
So now if we link this interface with our class, we could modify test the following way:
I hope you haven't forgot about main interface feature, which allows class to implement as many interfaces as it could, yet? Well, it's a good time to implement custom
PageFactory then, isn't it?
Let's move back to
core module. We need to modify
BaseTest class for creating
PageObjects' storage. You may know that
PageObject pattern assumes that we'll often return a new instance of a page. But in case of delayed elements' search (
By locators), do we really need to create redundant objects in memory? In such context it's better to think about page caching. Let's say we could avoid creating new objects, if page already exists in a storage. But with 1 small note: storage should be refreshed after each test execution to avoid keeping useless objects in memory for a long time. Let's see how could it look like:
Normally, we may want to hide storage from outside word. So only getter was made
public. Here we're using
TestNG specific annotation to automatically clear storage after each test execution. Storage itself is a common map, where value is a page object instance and key is of generic interface type. Let's see how it looks like:
Here you can see 2 static methods: first one is about providing page object instance by key, and second one - our magic navigation method. But it doesn't return any
PageObject yet, intrigue. Also we may want to define a special
create method, which is called while putting values into storage. But actual implementation left to higher abstraction layers (if you remember, we've discussed a role of framework as an independent unit a bit earlier).
The final piece of a puzzle lays in domain layer. Now we need to provide a more specific page objects creation logic. And as you may guess, it'll be implemented via another interface. Let's call it
PageObjectsSupplier:
First thing we may noticed is a
PageObject enum, which implements just created
GenericPage. As you remember, we have previously defined an abstract method
create to pass implementation details to domain specific area. So
PageObject must implement this method now. As it has an enum type, each unique item will provide its own implementation. Exactly what we need!
There're also 3 default methods. You had a chance to see
loadUrl before in provided above test implementation. So we've just wrapped original navigation method defined on a
core level with a domain specific logic of returning new
LoginPage instance. As this method is default one, we could call it directly in a test.
Others are just common page objects getters to avoid direct low-level
getPageObject method's calls with type casting. So it's just some kind of synthetic sugar for more concise instance access. Note that we use
putIfAbsent method for populating pages' storage. It means that there will be created only 1 instance of particular page. Well, it may seem a bit excessive to define both enum items and relative getters, but on the other hand it's technically and logically clearer than hundreds lines of reflection or just separated utility class. Plus we found a better place, where to store first navigation logic. Anyway, it's only an alternative approach and it's up to you what to choose.
Now we only need to connect newly created interface with our test class to apply multiple inheritance magic. Just a quick suggestion: to avoid excessive interfaces' enumeration, we could join them together in 1 more specific interface e.g.
TestCase using inheritance.
So our final test case variant would be the following:
If we run this test and then generate
Allure Report via
mvn site command, we'll see all the steps, including verification. Isn't it look perfect?
Note that I'm using my own web sever for viewing reports. You may want to read official
Allure docs to find out a list of available maven commands.
In a second part we'll take a look at more complicated and interesting example with custom
PageElements. Source code will be also available later on GitHub.
Hope this article helped you to get better understanding of
default methods and how they could improve your automation routine.