In the post on using page objects with Symbiont, the focus was on how Symbiont leverages the page object to allow test scripts to separate intent from implementation. This also allowed for test script logic to be concise in terms of its expression. Here I’m going to focus on how we can get even slightly more concise by adhering to a factory pattern.
In the last post on this subject, you ended up with a page.rb file like this:
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 51 52 53 |
class Symbiote attach Symbiont url_is 'http://localhost:9292' url_matches /:\d{4}/ title_is 'Symbiote' p :login_form, id: 'open' text_field :username, id: 'username' text_field :password, id: 'password' button :login, id: 'login-button' div :message, class: 'notice' def login_as(data) login_form.click if data.is_a?(Hash) username.set data[:username] password.set data[:password] end login.click verify_successful_login end def verify_successful_login expect(message.text).to eq('You are now logged in as admin.') end end class Weight attach Symbiont text_field :weight, id: 'wt' button :calculate, id: 'calculate' def convert(value) weight.set value calculate.click end end class Navigation attach Symbiont p :page_list, id: 'navlist' link :weight_calculator, id: 'weight' def navigate_to(location) page_list.click self.send("#{location}").when_present.click end end |
You also had a script.rb file like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
require 'rspec' include RSpec::Matchers require 'symbiont' require './pages' Symbiont.set_browser page = Symbiote.new page.view expect(page).to be_verified page.login_as ({ username: 'admin', password: 'admin' }) page = Navigation.new page.navigate_to(:weight_calculator) page = Weight.new page.convert('200') |
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. Each line above that instantiates a new page object and stores it in page
is establishing that context. This does come at the expense of adding a bit of boilerplate to the code.
Since Symbiont encourages you to use the 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 Symbiont factory and then make calls to defined factory methods.
Here’s a revised script:
1 2 3 4 5 6 7 8 9 10 11 12 |
require 'rspec' include RSpec::Matchers require 'symbiont' require './pages' include Symbiont::Factory Symbiont.set_browser on_view(Symbiote).login_as ({ username: 'admin', password: 'admin' }) on(Navigation).navigate_to(:weight_calculator) on(Weight).convert('200') |
In particular, note that the above code is including the Symbiont factory in this script as a mix-in. With the Symbiont factory available, you can make calls to a series of on
context methods. Breaking it down a bit, consider just this:
1 |
on_view(Symbiote) |
Here, the on_view
is a context action that says “on viewing the page that is represented by the Symbiote page definition.” In this case, the context knows to use the browser driver instance, if you provided one, or the built in instance that Symbiont will create, if you did not provide one. This context allows you to act on element definitions directly or on methods directly. The result of this is an instance of Symbiote, which means you can call methods on it, such as login_as
.
This does bring up a good point, worth calling out. Just as you can call the methods on your page definition, you could use the factory to call element definitions as well. So if you think back to the original logic from the first post, you could use a series of factory calls like this:
1 2 3 4 |
on(Symbiote).login_form.click on(Symbiote).username.set 'admin' on(Symbiote).password.set 'admin' on(Symbiote).login.click |
That perhaps reads a little messier but it does work. That, however, brings up another point worth considering: factories can take blocks. So the above could be written as:
1 2 3 4 5 6 |
on(Symbiote) do |page| page.login_form.click page.username.set 'admin' page.password.set 'admin' page.login.click end |
Going back to the on_view approach, I should note that “on view” can be read as “on viewing”. This means that the page will be called up in the browser, which further means the page definition must have a url_is
assertion. This action will also perform the verified?
check if your page definition includes url_matches
and title_is
assertions. If those assertions are not there, the verified?
check will not be run.
Notice the next two lines do not use on_view
, but instead use on
. Why the difference? In those case 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 the navigate_to
method to go to the weight calculator page. The on(Weight)
logic thus is able to convert a weight of 200.
Make sure you understand how the factories create instances. This is what allows Symbiont to reuse existing definitions so that the context is not lost and so that you are not establishing a new browser for every single test.
Factory Instances
The context factory methods will establish new instances of page or activity definitions but will also cache existing definitions so that they can be reused. For example, consider this simple script:
1 2 3 4 5 6 7 8 9 10 |
require 'symbiont' include Symbiont::Factory class Symbiote attach Symbiont url_is 'http://localhost:9292' end Symbiont.set_browser |
Now consider the following statements:
1 2 3 4 5 |
on_view(Symbiote) puts @page on(Symbiote) puts @page |
The output you will get is something like this:
#<Symbiote:0x38f6d30> #<Symbiote:0x38f6d30>
These calls lead to two objects that are the same, which you can tell by their hex identifier. A call to a context factory provides an active page instance. In between each call, the above code shows you that @page
is the same instance. Also note that this @page
instance variable could be used just like you used the page
variable in the original version of the script I started with at the top of this post. I want to make sure this is clear, so look at this logic again:
1 2 3 |
@page = Symbiote.new expect(@page).to be_verified @page.login_as ({ username: 'admin', password: 'admin' }) |
Realize that the above is replicated by this:
1 |
on_view(Symbiote).login_as ({ username: 'admin', password: 'admin' }) |
The call to on_view
makes the @page
variable available but also hides it unless you specifically want to call it. The idea of a context factory is that you use it for all of your automated test logic.
In my sample script where you ended up with two identical instances of Symbiote, what that is showing you is that the definition in question is cached so that it can be used during a given test execution. That works out good but what if you want a new instance for some reason? You can specify that a new context should be created. Consider this slightly modified logic:
1 2 3 4 5 |
on_view(Symbiote) puts @page on_new(Symbiote) puts @page |
The output will be something like this:
#<Symbiote:0x38f2a70> #<Symbiote:0x3922b90>
Here you can see there are two different objects. And, as you would suspect, that is as a result of the call to on_new
. What the above code shows is that the context can be different when a new instance is specifically requested.
Factories and Simplicity
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.