In the previous post I talked about communication patterns in terms of the micro-framework and tests. Here I’ll talk about the expressiveness of the tests themselves, showing how Tapestry supports the idea of a context.
This is what we ended up with in the last post:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
require 'webdriver_manager' require 'rspec/expectations' include RSpec::Matchers require 'tapestry' class Veilus include Tapestry url_is 'https://veilus.herokuapp.com' img :test_gorilla, id: "site-image" element :open, id: "open" text_field :username, id: "username" text_field :password, id: "password" button :login, id: "login-button" element :notice, class: "notice" element :navigation, id: "navlist" link :stardate_on_nav, id: "stardate", index: 1 img :stardate_logo, id: "stardate-logo" def begin_with move_to(0, 0) maximize end page_ready { [test_gorilla.exists?, "Veilus logo is not present"] } end Tapestry.set_browser page = Veilus.new.view do |action| action.open.click action.username.set! "admin" action.password.set! "admin" action.login.click end expect(page.notice.text).to eq("You are now logged in as admin.") page.navigation.wait_until(&:dom_updated?).click page.stardate_on_nav.wait_until(&:dom_updated?).click expect(Tapestry.browser.title).to match 'Stardate Calculator' expect(Tapestry.browser.url).to match 'stardate' expect(page.stardate_logo).to exist Tapestry.quit_browser |
As I indicated in that last post, one of the challenges that was becoming obvious as we delegated more of the test script to the page definition is that we only had one abstraction or one definition: Veilus. Consider just those “begin_with” and “page_ready” aspects I showed you. Those are relevant when first going to the site, but what about when we get to the Stardate Calculator, as we do at the end of the script?
As with any framework, you eventually run into areas where it can seem like the organizing principle is conflicting with mechanics that the principle itself provides. And it can leave you wondering how to structure your test logic. But a good micro-framework should help you navigate those choppy waters by essentially doing nothing more than giving you options. So let’s paint ourselves into a few of these corners and see how I might handle this with Tapestry.
Modularize Definitions
Rather than me boring you to death with a lengthy explanation of what I’m doing, how about I just trust you to understand the basics? So, first, here’s a breakdown in the script of more definitions.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
class Veilus include Tapestry url_is 'https://veilus.herokuapp.com' img :test_gorilla, id: "site-image" def begin_with move_to(0, 0) maximize end page_ready { [test_gorilla.exists?, "Veilus logo is not present"] } end class Login include Tapestry element :open, id: "open" text_field :username, id: "username" text_field :password, id: "password" button :login, id: "login-button" end class Landing include Tapestry element :notice, class: "notice" end class Navigation include Tapestry element :navigation, id: "navlist" link :stardate_on_nav, id: "stardate", index: 1 end class Stardate include Tapestry img :stardate_logo, id: "stardate-logo" page_ready { [stardate_logo.exists?, "Stardate logo is not present"] } end |
Take a moment to look that over and orient yourself. Note that these are not all “page definitions” (page objects). The authentication is a form that pops out. The navigation is, similarly, not a page but rather just an element that pulls out from the left side of the screen.
Here you can see how the element definitions are now spread across multiple definitions. This is the way that Tapestry essentially supports the common “page object pattern”, if we want to call it such. But this introduces some challenges to our script. I’ll encourage you to look at the script portion as it is and see why that is. Let’s modify the script like this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
Tapestry.set_browser Veilus.new.view.check_if_ready page = Login.new do |action| action.open.click action.username.set! "admin" action.password.set! "admin" action.login.click end page = Landing.new expect(page.notice.text).to eq("You are now logged in as admin.") navigate = Navigation.new navigate.navigation.wait_until(&:dom_updated?).click navigate.stardate_on_nav.wait_until(&:dom_updated?).click Stardate.new.check_if_ready Tapestry.quit_browser |
Again, take a moment to orient yourself. Here I’m doing things that I’ve already basically talked about, in terms of how to structure the code. So I’ll count on you to see what I’m doing and why. What should be immediately clear is that I’m simply instantiating pages. Then for each page, I’m calling actions. One thing to note is a slight change to how I’m checking for the “page ready” aspects. That’s shown in these two lines:
|
1 2 3 |
Veilus.new.view.check_if_ready ... Stardate.new.check_if_ready |
Here I’m just appending a “check_if_ready” call to the instantiation of the definition. This is because, right now, I’m not taking any action on those pages. So I can’t use a “when_ready” block like I showed you in the last post. Also note that in the case of Veilus, I’m calling “view” because that’s the scripts first interaction, which means I need to go to the URL. But for the Stardate page, I don’t call “view” because my script will go to the page as a result of its execution.
And that brings up a further point: notice how only the Veilus definition contains a “url_is” attribute. The others do not. You can put a “url_is” attribute on any definition you want if you want to simply go to that page directly. But, in the case of my Veilus application, you actually can’t go directly to the Stardate page because you need to login in first.
Delegate Actions to Definitions
Let’s handle another bit of low-hanging fruit. That’s the idea of wrapping sets of related actions up into a method and putting that method on the relevant definition. Here are some definitions with added methods:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class Login include Tapestry element :open, id: "open" text_field :username, id: "username" text_field :password, id: "password" button :login, id: "login-button" def login_as_admin open.click username.set! "admin" password.set! "admin" login.click end end class Landing include Tapestry element :notice, class: "notice" def verify_admin_login expect(notice.text).to eq("You are now logged in as admin.") end end class Navigation include Tapestry element :navigation, id: "navlist" link :stardate_on_nav, id: "stardate", index: 1 def go_to_stardate navigation.wait_until(&:dom_updated?).click stardate_on_nav.wait_until(&:dom_updated?).click end end |
With those in place our test script can become this:
|
1 2 3 4 5 6 7 8 9 |
Tapestry.set_browser Veilus.new.view.check_if_ready Login.new.login_as_admin Landing.new.verify_admin_login Navigation.new.go_to_stardate Stardate.new.check_if_ready Tapestry.quit_browser |
Notice how this is nothing more than a simple abstraction of instantiating one of the definitions and then calling a method on it.
Context Factories
When using page or activity definitions, the context is critical because it’s how your automation script knows where to execute a portion of its logic. In fact, the definition is the context. So the key thing from the script above is that you are always getting a page context that you can operate on.
I purposely reduced the script to these bare essentials to make it clear what a context factory is doing. Since Tapestry encourages you to use a definition pattern (which can be treated as a page object pattern), it accommodates that by providing a factory behind the scenes that will make establishing the context of a page object a little easier and read a little nicer. To do this you have to include the Tapestry factory and then make calls to defined factory methods.
Tapestry provides a factory module for this purpose. Let’s add an inclusion for the factor near the top of the script:
|
1 2 3 4 5 6 7 8 |
require 'webdriver_manager' require 'rspec/expectations' include RSpec::Matchers require 'tapestry' include Tapestry::Factory ... |
Now let’s change just the test script portion to look as such:
|
1 2 3 4 5 |
on_view(Veilus).check_if_ready on(Login).login_as_admin on(Landing).verify_admin_login on(Navigation).go_to_stardate on(Stardate).check_if_ready |
With the Tapestry factory available, you can make calls to a series of “on” context methods. There’s also the “on_view” context method. So let’s break this down a little bit. The “on_view” factory is a context action that says “on viewing the definition that is represented by the Veilus page definition.” The “on_view” part also means that the definition will be called up in the browser, which further means the page definition must have a url_is assertion.
The next lines do not use on_view, but instead use “on”. Why the difference? In those cases, since a page has already been viewed, and thus a browser initialized, the test script just needs to state that the following actions are taking place “on” a specific page. They don’t need to view the page because the previous action is taking the script to the correct spot. For example, after logging in, the navigation becomes available. The on(Navigation) logic is using then going to a Stardate page.
There’s a lot more I could say about Tapestry’s factory pattern but here I just wanted to get you exposed to it.
Expressive Test Scripts
The main point to get out of this is a very simplified test script, that removes a lot of boilerplate, and that allows you to concisely express the intent of your tests by making the context and action being taken very clear. Further, with the delegation of actions to the page objects, your tests become very compressed but without, I would argue, sacrificing clarity. Even further, those elements can, as you see, start to be composed as workflows.
Modular Design Allows Extensions
Tapestry itself does not support workflows directly but, as you can see, its organizing principle is flexible enough to support a variety of expression modes for tests. Further, since Tapestry is a micro-framework, it’s designed to allow other solutions to be used with it. So, for example, if I did want to consider using workflows more directly, I wrote a test_workflow gem that helps with that.
Tapestry is about functional execution in a singular performance context. But it doesn’t report on performance measures at all. Yet if I want to get performance measures while my script is running, my test_performance gem can provide some value there.
Tapestry doesn’t have much to say about data building or providing accessible data. But if I want that, my data_builder or data_accessible gems will provide those mechanisms.
My goal here is not to promote my own tools although it may appear that way. Rather, my goal is to show why I wrote these various tools and, in particular, why those tools can stand on their own but also be incorporated by a micro-framework.
Are We (Finally!) Done?
Well, I think there actually might be value in one more post on this topic. I’d like to do a little breakdown for you of how Tapestry works by referencing specific aspects of its code, more than I have already, and giving you some insight into why the micro-framework was created as it was.