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_managerInstalling 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:
1 2 3 4 |
require 'tapestry' puts Tapestry.version puts Tapestry.dependencies |
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.0Way 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
require 'webdriver_manager' require 'rspec/expectations' include RSpec::Matchers require 'tapestry' include Tapestry Tapestry.set_browser Tapestry.move_to(0, 0) Tapestry.maximize Tapestry.visit 'https://veilus.herokuapp.com' expect(Tapestry.browser).to be_an_instance_of(Watir::Browser) expect(Tapestry.browser.driver).to be_an_instance_of(Selenium::WebDriver::Chrome::Driver) Tapestry.quit_browser |
1 |
browser = Watir::Browser.new |
1 2 |
Tapestry.set_browser :firefox Tapestry.set_browser :phantomjs |
1 |
Tapestry.set_browser :chrome, headless: true |
1 2 3 4 5 6 |
Tapestry.set_browser :chrome, switches: %w[--ignore-certificate-errors --disable-popup-blocking --disable-translate --disable-notifications --disable-gpu --headless] |
Browsers and the API
Right now Tapestry is basically acting as a wrapper for your browser. So you can see calls tomove_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:
1 |
puts Tapestry.watir_api |
1 |
puts Tapestry.selenium_api |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
require 'webdriver_manager' require 'rspec/expectations' include RSpec::Matchers require 'tapestry' include Tapestry class Veilus include Tapestry end Tapestry.set_browser Tapestry.move_to(0, 0) Tapestry.maximize page = Veilus.new Tapestry.visit 'https://veilus.herokuapp.com' expect(page).to be_a_kind_of(Tapestry) expect(page).to be_an_instance_of(Veilus) Tapestry.quit_browser |
1 2 |
puts Tapestry.browser.inspect puts page.browser.inspect |
#<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:
1 |
puts page.inspect |
#<Veilus:0x007f95f84aeaf8 @browser=#<Watir::Browser:0xceed71548bdba0 url="https://veilus.herokuapp.com/" title="Veilus">>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:
1 |
puts Tapestry.api |
1 |
puts page.definition_api |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
require 'webdriver_manager' require 'rspec/expectations' include RSpec::Matchers require 'tapestry' include Tapestry class Veilus include Tapestry end Tapestry.set_browser Tapestry.move_to(0, 0) Tapestry.maximize page = Veilus.new Tapestry.visit 'https://veilus.herokuapp.com' expect(page.title).to match 'Veilus' expect(page.url).to match 'herokuapp' Tapestry.quit_browser |