Let’s Build A Framework – Day 2


Fleshing Out Our UI Automation Framework

Hello again to all the West Denver Test Engineers, or any wayward internet traveller who stumbled across this page.  Today we are going to go over what we built in Day 2 as part of our great framework building experiment.   The good news, is this blog post should be much shorter than Day 1, which had to include a lot of ancillary explanation of the tools we were using.   The bad news? were nearing the end of our journey.

 

So let’s get to it shall we?

What Are We Going to Build Today?

Today we are going to expand our framework with some new killer features like automatic frame handling in our tests.  And introducing a way to make our tests even more readable with a page object collection.  Finally we will close with adding a great new feature that will allow us to validate any page we write is fully loaded without ever having to write new code.

A way to clean up our test readability

Before we get started let’s revisit what our last test looked like at the end of Day 1.

it('Should open ebay and search for orphans', function() {
   //I want to open a browser
   //I want to load google
   var ebayHome = new EbayHome(driver);
   return ebayHome.open()
   .then(function() {
     return ebayHome.searchFor('orphans');
   });

});

It’s a little cleaner, but if we imagine a test spanning numerous pages we’ll have to create each page object every time.

it('Should do lots of actions', function() {
  var ebayHome = new EbayHome(driver);
  var ebaySearchResults = new EbaySearchResults(driver);
  var ebayUserPage = new EbayUserPage(driver);
  // you get the idea
});

Not only is this not pretty, it exposes a potential maintenance problem.  Say at some point in the future, we decide we need to pass an additional argument to our page objects instead of just the driver.  In this case we would need to update every test and every instantiation of every page to support that.

A maintenance nightmare.

To solve this we will build out a PageObjectCollection object.   This will be a simple class that stores all our pages, so we can simply ask for which page we want.  Through some clever naming, this will also greatly improve the readability of our tests.

A way to handle frames

For those of you who have experience with older websites, or sites that use a lot of iframes you probably know that frames can be incredibly frustrating to deal with in UI automation.  The reason being that even though you can see everything on the browser webdriver treats each framew like its own window.  And you have to switch to that window to see the content.

The result is a lot of test code like this:

driver.frame('frameName')
.click('some locator')
.frame('back out of content')

Wherein we constantly have to switch into and out of frames in our tests.  And if someone forgets to switch back out tests might fail leaving a wake of very confused test engineers trying to debug.

We are going to expand our locator file to allow us to indicate if this element lives in a separate frame, and enhance our commands to automatically change into and out of a frame.  This will allow testers to write actions in peace without ever having to think of frames again.  Truly a marvelous future.

A way to automatically be able to validate any page has loaded correctly

Finally we are going to add our most powerful feature to date.  We are going to add a way through the locator files to “Tag” elements as being required for the page to be considered loaded.   Then we will write a new method that will be shared for all pages that automatically checks for every element thats required.

This gives us the opportunity to write easy smoke tests that checks every page of the application for missing elements.  It also allows us an elegant way in our test to wait for the page to load before proceeding, without resorting to ugly waits or polling functions polluting our pristine test waters.

Getting Our Hands Dirty

Now that we know what we want to do.  We can get down to the nitty gritty and build it out.  Let’s start with the page object collection.

Introducing the Page Object Collection

As mentioned above the goal for the page object collection is to give us a way to ask for any page we want and give us a fresh page object for it.   We can then include and initialize the page object collection in all of our tests.   And if we are clever we can give it a name like “on”.  This will result in our test code looking like this:

on.ebayHome().open()
.then(function() {
  return on.ebayHome().searchFor('orphans');
});

Much more readable.  The “on” makes it read like english, so even non programmers can get an idea of what the test is doing.

Creating the collection

We’re going to put our collection in the pages directory.  So to start we want to create a file called page_object_collection.js.   This is going to be a class, not a library, so it’s going to look a lot like our page object.

Screen Shot 2016-11-17 at 12.40.12 PM.png
Basic structure of page object collection
  • Note that we have to require all of our pages at the top of the file.
  • Just like a page we have a constructor that takes the driver in.  We save this driver so we can pass it to any page the user wants.
  • Finally we add a “prototype” function for every page.  In this case we made the function name camelCase to stick with javascript conventions

So if you had 10 pages you can add a method for each page.  Then in your test you can reference any page with the structure

on.<pageName>().<pageAction>()

Implementing the functions is easy as can be:

PageObjectCollection.prototype.ebayHome = function() {
   var self = this;
   var ebayHome = new EbayHome(self.driver);
   return ebayHome;
};
  • Here we use the same “self” convention just for good coding practices
  • We create the page object just like we would do in our test and then we return it.
  • If you are wondering, nothing being done here involves I/O so we don’t need to wrap anything in a promise.  We are simply instantiating a page, but that has no impact on the actual browser or WebDriver.io.

Our finished result (with a stubbed out function for another page we want to add looks like this:

Screen Shot 2016-11-17 at 12.46.49 PM.png
Page Object Collection Finished Product

Now that we have our collection, we should modify our test to take advantage.

Modifying the tests to use the collection

For now we’re going to stick with our “tests/better_testing.js” file.  But we don’t want to lose our old test, so we’re going to temporarily disable it, and create a new test underneath it.

You can disable a test in mocha by adding “.skip” after the it statement like so:

it.skip('Should open ebay and search for orphans', function() {

As mentioned in day 1, you can have multiple tests per describe block, so we can add a new test right underneath our first one.

Note:  be careful to ensure you are adding your new test after the ‘});’ that closes the it block.  A common bug is someone accidentally tries to add their test inside an existing test.

it('Should open ebay and search for orphans but better this time', function() {
  //Test will go here
});

Obviously before we can use our page object collection we have to define it somewhere, and because its a class we have to initialize it.  The first step here is require it:

var PageObjectCollection = require('../pages/page_object_collection');

Note again, because its a class and not a library we use Capital first letters to indicate to people.  This is a common Node code convention.

As we discussed, we want to store our collection in a variable named “on” for readability.  We can define on next to where we define our browser near the top of the test.

var driver;
var on;

Next we need to create a new instance of the collection and assign it to our “on” variable.  Since we need to pass the driver in on creating it, we should probably do this before every test.  (Since the driver is closed at the end of every test).

So near the bottom of our beforeEach we can do this:

  on = new PageObjectCollection(driver);

Now we are cooking with grease.  And we can re-write our previous test but this time using our collection.

 return on.ebayHome().open()
 .then(function(result) {
    return on.ebayHome().searchFor('orphans');
 });

Much more readable.

Our final result looks like this:

Screen Shot 2016-11-17 at 12.57.22 PM.png
Improved test using page object collection

Note: we shrunk some of the file to fit it all in the screenshot, don’t worry we still have our afterEach and first “it” test they are just minimized.

One goal down for the day 2 to go

Handling Frames Through the Locator File

Frames are a tricky thing, some engineers can go years without ever running across one while others must deal with them constantly.  Still building support for frames in your framework even if you don’t ever use a frame, could pay off down the line if you want to embed youtube or other site content suddenly. You’ll be able to support it with no effort.

To support frames we want to make it an optional field in our locator file.  Meaning if an element lives in a frame we want to add a field called “frame”:

 "topSearchBar": {
   "description": "Search bar at top of the page",
   "locator": "#gh-ac",
   "frame": "contentFrame",
   "groups": ["requiredForPage"]
 },

Thats it for the locator file.  Now we want to modify our custom commands, to check if a locator has a frame, and if so to switch into that frame before running the command.

We’ll start with our “click” method.

Here we’ll do some actual node coding.  To check if a field exists in an object we just do

if (locatorObject.frame) {
//locatorObject has a property or function called frame
}

Then we will take advantage of the WebDriver.io chaining ability and just add a method to switch to the frame before our click, and one after to switch back to the main frame.

 return resolve(driver.frame(locatorObject.frame)
 .click(locatorObject.locator)
 .frame(null));

note that we keep the entire chain inside the resolve statement to ensure our promise works.

Then, we handle the case if an element does not have a frame by adding a else statement. In which we call click as we did before.  The whole thing looks like:

Screen Shot 2016-11-17 at 1.50.19 PM.png
Updated click command with frames

And we can update setValue the same way

Screen Shot 2016-11-17 at 1.51.55 PM.png
Updated setValue command with frames

And that’s all there is to that.  Both methods will now handle switching into a frame for any element that requires it, and switches out when done.

Building Out the “waitForPageToLoad” Feature

Now we’ve come to our last new feature we want to add as part of Day 2.   The feature of waitForPageToLoad.  Building this we’ll get to learn some new concepts about promises, and programming through loops.

Adding a tagging system to our locator files

The first thing we want to do is to be able to tag elements in our locator file to indicate this element should be present before the page can be considered loaded.  To do this, we are going to add a new field called “groups”, this is going to be an array where we can put whatever group we want this element to be a part of.

Our group is going to be “requiredForPage”.

Why make it an array?  Because we might find other ways and reasons we want to group elements so in thinking ahead we want to support it right off the bat.

Since we only have 2 elements so far, and both should always be present on the EbayHome page we’re going to mark them both as required like so:

{
 "topSearchBar": {
 "description": "Search bar at top of the page",
 "locator": "#gh-ac",
 "frame": "contentFrame",
 "groups": ["requiredForPage"]
 },

 "topSearchButton": {
 "description": "Search button at top of page",
 "locator": "#gh-btn",
 "groups": ["requiredForPage"]
 }

}

Add a new custom command to wait for a single or group of elements to be visible

Our next step is to add a way to tell if an element is visible.  If we want to wait until all elements are loaded we’re going to need this.  As an added benefit these methods will be available for future uses as well.

We’ll build these by going off the existing “waitForVisible” command in WebDriver.io.  And we’ll add our own waitForVisible methods in our commands.js file.

Luckily the code is pretty much identical to the code for click, the difference being we change our console output and we call waitForVisible instead of click.

 

Screen Shot 2016-11-17 at 2.23.21 PM.png
New waitForVisible command

Note: because the code is so similar you can get away with copying and pasting, but I highly recommend you re-type it out.  First for that muscle memory learning, and second to encourage us to find ways to avoid duplicating code.   We don’t have time in this framework but we could build a simple helper that we just pass our method to that avoids all the duplicated code.

Now that we can wait for 1 element, next is we want to wait for a group of elements.  This will be a little more complicated.  We’re going to want to create a new command called
“waitForGroupToBeVisible”.

 waitForGroupToBeVisible: function(locatorGroup) {


 },

We’re going to assume someone has already compiled a group of elements into an array for us so we take a locatorGroup as an argument.

So we’ve done well with promises so far.  But in situations like this where we need to do multiple promises in a loop situation we need to use a special function our promise library has called “all”.

The “all” method in our library will take an array of promises, and run them all, and when it is finished it will return its own promise, with an array of results.  If any of the promises fail, it will fail as well.  The bonus is it runs through all these promises at the same time, which saves our program time as well.

var promises = []
promises.push(promiseFunction());
promises.push(promiseFunction2());

promises.all(promises)
.then(function(results) {
    //At this point we know all promises are done
});

Above is the basic form of using “all” for promises.

So we are going to use the same concept.  First we create an array to hold our promises

var promises = [];

Our next step is we want to loop through every locator in our locatorGroup and we’ll want to check each one if its visible.

locatorGroup.forEach(function(locator) {
    //Check if this locator is visible and push on promise array
});

This is an example of a “forEach” loop a very nice tool in Node/JavaScript.   It is a function that is called for each element of an array and for each element it will execute the function passed in.  Here it passes us a locator.

Our next step is to wait for it to load, which, lucky us, we just happened to write a function that does that!

locatorGroup.forEach(function(locator) {
    promises.push(module.exports.waitForVisible(locator));
});

So here were using a feature of arrays in javascript that we can add elements to an array with the “push” method.  We’re also using a feature that lets us call other functions  in the same library.  You can do this by appending module.exports.<functionName>.

So we basically go through each element call our waitForVisible method and store them all in an array of promises.  It’s important to note at this point in the code none of the promises have resolved they are all working at the same time.

So our final step is to wait until all promises are done.  Enter our “all” function.

return Promise.all(promises)
 .then(function(result) {
     return result;
 });

As we discussed in the last day, that “return” statement is critical, if left off the method will end before any promise has finished.

That’s it we can now wait for a single or group of elements!

screen-shot-2016-11-17-at-2-38-59-pm
New waitFor visibility elements

Write our waitForPageToLoad method

Now all thats left is to add a method to our page, that will find all the elements that are required and call our new custom command.

For now we are going to add it directly to our EbayHome page, but in a future post we’ll talk about how to make this method available to every page by default.

So we are going to edit pages/EbayHome.js and add a new internal function to findRequiredLocators.  And to add a new public method called waitForPageToLoad

var findRequiredLocators = function(locators) {

};

EbayHome.prototype.waitForPageToLoad = function() {

};

So what the hell is going on here, why do we have this new way of defining a function?  When we define a function through “var myFunction = function” we are limiting the scope of that function.  People using our Page Object in a test will not be able to access our findRequiredLocators function, but they will be able to call our waitForPageToLoad function.  Because we added waitForPageToLoad as a prototype of our class.

However waitForPageToLoad will be able to call findRequiredLocators because they are in the same scope.  It’s crazy but it will eventually make sense I promise.

Lets start with findRequiredElements.  In the page we already have all the locators stored in our “locators” variable.  But woe be unto us, the locators variable is not an array, its an object, with each property referencing a element.  Because of this, we cannot use forEach to loop through, because thats for arrays.

Instead we are going to use a helper library called “lodash”.  We store lodash in a variable called “_”.  Making sure to pull it in from a require statement.

var _ = require('lodash');=

LoDash has a very handy method called “forOwn” this will loop through all the properties of an object like a forEach loop.  It will give us two values (key and value).  The key is the name of the element and the value will be the locatorObject.

var returnArray = [];
_.forOwn(locators, function(locator, keyName) {

    //Now we can check if a locatorObject is part of the requiredForPage group
 });

To  check if a locator in our loop is part of the requiredForPage group is pretty simple

if (locator.groups) {
    if (locator.groups.indexOf("requiredForPage") !== -1 {
        //found a match
        returnArray.push(locator);
    }
}

So first we check if it even has the groups field, if it has no groups we know its not part of requiredForPage so we can do nothing.

Next we use a feature called “indexOf” this will return the index of an array that matches, if it can’t find a match it will return a -1.  You can think of this as saying “if locator.groups contains “requiredForPage””.

So we are saying if it doesn’t equal a -1 it means this locator does have this group.  In which case we push it onto our result array.

Finally when were done we just return our array, which now contains all elements that are part of the requiredForPage group.

screen-shot-2016-11-17-at-3-01-34-pm
New findRequiredLocators helper function

Once we have this method, our waitForPageToLoad method is trivial.  We just find our elements and pass it to our custom command.

Screen Shot 2016-11-17 at 3.03.38 PM.png
Our official waitForPageToLoad method

 

That’s it!  It took some work, but from now on anytime we want to check if a page is ready we can call this function, and any new page we can tag which elements are required with a few keystrokes.

For our final piece lets see how this looks at our test level.  We’ll go ahead and modify the new function we added earlier …

Screen Shot 2016-11-17 at 3.05.26 PM.png
Beautiful Isn’t It?

What’s Next?

So now hopefully we are starting to see some of the benefits of our framework.  We are going to do a special day 2.5 session next.  Which will exist outside of the meetups and only on this blog.   In that we are going to do one quick stop gap fix to create a base class that all of our page objects will sit on top of.

This is going to let us use that waitForPageToLoad method on all pages.

After thats added, for day 3, we will make some finishing touches on the UI framework.   And we will switch over and build a quick API Testing component to allow us to test APIs as well.

Truly we live in interesting times.  If you made it this far I commend you, and keep your ear to the ground for the next post which should come in a week or so.

 

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s