Writing Automation Micro-Frameworks

Here I want to talk a little about test automation framework construction. Or, rather, micro-framework construction. I will use my own tool, called Tapestry, for this purpose. Tapestry is written in Ruby but what I talk about is potentially transferrable to your language of choice.

Not only do I believe but I know that Ruby is one of the best languages and ecosystems in which to build test solutions for automation against a UI, service, or an API. I’m not talking about building enterprise solutions or apps or whatever else here. I’m talking about test solutions. And that distinction does matter when you consider the ecosystem you want to use.

A few years ago I wrote about why test engineers should learn Ruby. And that was because of the ease of creating a simple test supporting tool in very few lines of code. Ruby is also fantastic even if your application itself is not written in Ruby. I wrote awhile back about how your automation language does not have to be your development language.

All this being said, I’m not a language bigot by any means. And I would love to create a version of Tapestry, with the same interface I discuss here, in multiple languages. The challenge being I doubt you could get as clean as expression as you can do with Ruby. But I would absolutely like to be proven wrong about this.

What Is Tapestry?

Tapestry is designed to place a thin wrapper around a library called Watir, which stands for Web Application Testing in Ruby. Watir is a library that automates actions within a browser, using the WebDriver API. Watir is in turn a wrapper for Selenium, which is what’s actually providing the WebDriver API.

The guiding idea of Tapestry is one that fuels any work I do on test solutions: a certain linguistic sophistication in the pursuit of concise programming.

The Linguistics of Automation

My goal with Tapestry was simplifying syntax and making each line of code easier to comprehend. This is important to me because Tapestry is used in the context of automation that supports testing. Thus automation is simply an expression of upstream test thinking. My view is that simplicity of expression encourages people to compose scripts that are, in turn, simpler and easier to comprehend. Short scripts are cheaper to build, easier to deploy, and faster to maintain. There’s also much less chance of accruing technical debt with this kind of approach.

All of this mattered to me in the creation of Tapestry because irrelevant complexity quickly becomes dangerous complexity. Simplifying small things, like syntax, can lead to simpler big things, like test scripts. I would also argue that a more natural syntax not only makes your programming life easier but it lets you focus on the problem you are expressing. This helps make common test script tasks simpler and your code more expressive about what’s actually being sought for as part of the test execution.

Tapestry Is a Micro-Framework

A micro-framework provides a focused solution, which means it does one thing and one thing only, instead of trying to solve each and every problem. While doing that one thing it does well, the micro-framework should do it while being expressive yet concise.

My view is that an efficient test framework is one that needs minimal refactoring to adapt to new changes in the target application. That can be an interesting viewpoint when your framework is meant to be used with many different target applications.

Along with this, Tapestry was designed around the idea that the test runner, the test library, and the tests themselves are entirely separate and that includes in terms of how they are versioned and deployed. This means you can change (and version) tests without changing the framework. In fact, a measure of progress is that you are changing your framework — test library and runner — less and less.

Let’s Dig In

To play along with the scripts I write in this post, make sure you have a Ruby installation and then run the following:

gem install tapestry
gem install rspec
gem install webdriver_manager

Installing Tapestry will automatically cause Watir to be installed, which in turn will make sure that Selenium is installed. I should note that RSpec is not needed here necessarily. The reason I include it is so that I can use its expectation matchers for some of these examples. Also, webdriver_manager is not strictly needed but I use it here to make a point.

Create a script called script.rb and put the following in it:

Well, that doesn’t do a ton. It does print some information for you:

Tapestry v0.3.0
watir: 6.8.4
selenium-webdriver: 3.6.0

childprocess 0.8.0
did_you_mean 1.1.0
diff-lcs 1.3
ffi 1.9.18
json 2.1.0
mini_portile2 2.3.0
nokogiri 1.8.1
openssl 2.0.3
psych 2.2.2
rspec-expectations 3.7.0
rspec-support 3.7.0
rubyzip 1.2.1
selenium-webdriver 3.6.0
tapestry 0.3.0
watir 6.8.4
webdriver_manager 0.2.0

Way too many tools don’t tell you their dependencies or the version of those dependencies they are relying upon. Tapestry does that. Will you ever need it? Perhaps not. But it’s a way of making the tool transparent.

Okay, let’s change our script a bit.

Here I require my own WebDriver Manager tool, which automatically downloads WebDriver binaries. And right there is a slightly important point. You might ask why that’s not part of Tapestry. If you are building micro-frameworks, however, those should do one thing and do them reasonably well. They have the ability to pull in other micro-frameworks or other libraries to augment their abilities. That’s what’s happening here.

RSpec is being used just because I use some expectations to show you what’s going on. Finally, I should note that Veilus is just a little web application I put together.

Let’s look at some of the other elements added after requiring and including Tapestry. I add an include statement for including Tapestry. We’ll come across this again but what this is doing is providing some elements of Tapestry to be available to the script. Line 9 above is equivalent to calling Watir directly like this:

Watir, by default, uses Chrome and thus so does Tapestry. You can specify another driver, like this:

Do note that PhantomJS is largely becoming deprecated and the recommendation is to use Chrome Headless, which you can do as such:

You could also set a more full set of switches if you wanted to, such as via this:

All of this is obviously important as the browser is a fairly important thing to establish.

Browsers and the API

Right now Tapestry is basically acting as a wrapper for your browser. So you can see calls to move_to and maximize, which do what you would expect with the browser window.

Then you see a visit call. This is using the part of the Tapestry interface; specifically the visit call. The details of the code don’t matter so much but you’ll notice that behind the scenes I’m delegating this down to Watir’s ‘goto’ method.

And that’s why I have those expectations in place. You can see that the Tapestry “browser” is an instance of Watir::Browser. Watir, as I mentioned, wraps Selenium and provides a “driver” instance to maintain that relationship. The second expectation above shows you that, in this case, the browser driver is of type Selenium::WebDriver::Chrome::Driver. If you were running with Firefox, this would be an instance of Selenium::WebDriver::Firefox::Marionette::Driver.

Let’s get a list of what the browser exposes for your use. Add this to your script, somewhere before the final statement that quits the browser:

This lets you see all the methods that Watir provides on the browser. And while you’re at it, add this next line which shows you the methods that Selenium is providing.

What you are getting there are the respective public aspects of the Watir and Selenium APIs. Again, this is Tapestry giving you insight into what it has available.

Hmm. Doesn’t Seem Like Much …

I know, right? Bear with me. Tapestry provides a convenience mechanism for automation with WebDriver — provided that you are willing to describe your application in terms of definitions. These definitions can be whatever you want. The most common would be page and activity definitions which are conceptually similar (in fact, identical) to the concept of page objects.

As is now widely recognized in the industry, “page objects” — with an emphasis on the word “page” — are somewhat poorly named. They don’t have to refer to a page: just a collection of related elements that may interact together. In many cases, this will be an entire page, but in other cases, it may be a part of a page or a component that is reused across many pages. In any event, with Tapestry, those definitions are proxied to Watir so that they can be used as the basis for executing automation against a web browser or web service.

Let’s consider an example:

The addition here is the class called Veilus. Veilus is nothing more than a Ruby class. I could have named this class anything I wanted. Since it’s the home page of the application, I could have, for example, simply named it Home. What makes this class a page definition is that Tapestry is mixed-in with it. Including Tapestry in the class means that the Veilus class is treated as a special kind of class with some extra functionality. So essentially what you do is turn a generic Ruby class into a Tapestry-specific class.

What those expectations are showing you is that the page instance is “a kind of” Tapestry. But the page instance itself is an instance of Veilus, which is the class of the page definition. Let’s look at what we’re dealing with here. Add the following to your script:

These will provide the following identical output:

#<Watir::Browser:0x2e9e36c7934b06f6 url="https://veilus.herokuapp.com/" title="Veilus">
#<Watir::Browser:0x2e9e36c7934b06f6 url="https://veilus.herokuapp.com/" title="Veilus">

This shows you that the browser object for Tapestry and the browser object for the page are one and the same. So Watir has been proxied to the page definition. Now try this:

That will give you this:


Here you’ll see how the browser object that we just looked at is contained in an instance variable called @browser. This is an instance variable on the page object. One of my core points here is that it’s very important for the mechanics of your micro-framework to be exposed such that people can see how to extend your framework if they want to.

So you should note that while there is a @browser instance behind the scenes, you do have to refer to it in the context of Tapestry. This is important in case you have other test frameworks that similarly create a @browser instance variable. In other words, this is what allows Tapestry to play nicely with other tools you may be using. Details are encapsulated in the Tapestry namespace. You might also notice how I can retain access to the driver library — Watir — and also the underlying driver for it — Selenium. This means I can access any methods that exist on those libraries.

The thing to understand is that a @browser reference is passed in to the page definition. Where does the @browser come from? The call to set_browser is what created the @browser instance. Passing that @browser instance to the page definition means that a Tapestry controlled page object will be wrapped around the driver.

The Tapestry API

Let’s investigate a bit more into Tapestry. Try this:

The following will tell you the above along with everything that Tapestry provides to a page instance. This is effectively giving you some insight into the Tapestry API, which is attached to a page definition. You can also do this:

Right now that’s not going to provide you with much. In fact, it will provide you with exactly nothing. I’ll come back to that at another point in another post.

Using Tapestry, Using Watir, Using Selenium

Let’s add a few checks to your script. Here’s a version of the script with the two lines you should try out:

These delegate down to the @browser instance (see title and url). So note that Tapestry’s API is matching that of the library it wraps, Watir, when it makes sense to do so. This means someone doesn’t have to learn a new API just to use Tapestry, but rather can leverage one they know (Watir). And if you look at the Watir source (title and url) you’ll see that these just delegate down to @driver, which as we saw earlier was Selenium.

So That’s an Initial Look

This is probably a good place to stop for now. It’s always easy to go way too overboard and show everything at once. What I wanted to do here was introduce you to Tapestry and show you a little bit about it, including a delegation pattern that I think is very important; namely, wrapping an API (Selenium) around layers that allow to provide your own API, consistent with that which you are wrapping, but that also allows you to provide a certain style of presentation.

What I think you also saw here was that Tapestry exposed various aspects of itself in a fairly easy way for you to consume. And all of this was done without a lot of apparent moving parts. There is more to show with Tapestry, of course, but this gentle introduction here is how I believe the development of micro-frameworks should start in a test automation context.

The next post will further dig into the API that I started with here.


This article was written by Jeff Nyman

Anything I put here is an approximation of the truth. You're getting a particular view of myself ... and it's the view I'm choosing to present to you. If you've never met me before in person, please realize I'm not the same in person as I am in writing. That's because I can only put part of myself down into words. If you have met me before in person then I'd ask you to consider that the view you've formed that way and the view you come to by reading what I say here may, in fact, both be true. I'd advise that you not automatically discard either viewpoint when they conflict or accept either as truth when they agree.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.