Symbiont and the Page Object

In the post on going from Watir to Symbiont, I talked about how Symbiont encourages you to delegate as much as possible to the page definition. The reason for this being that page definitions get turned into page objects, thus allowing your scripts to rely on the page object to handle functionality, rather than having each automated script do so. Here I’ll explore that idea a bit more.

In the previous Watir-to-Symbiont post, you ended up with the following:

Elements to Objects

Let’s focus on the lines that are actually interacting with the web page:

Those statements work because the page definition declares the following element definitions:

Let’s consider one of those lines in a certain amount of detail. Each of the selectors (p, text_field, button) are based on methods that exist within Watir-WebDriver. What you’re actually seeing there is a method call, which might be easier to see if you put in the parentheses:

That’s simple enough. Symbiont goes one step further, however. What happens is that the second argument to the element method — the friendly names (:login_form, :username, etc) — will be converted by Symbiont into methods themselves. Those methods return an instance of the Watir element they correspond to. Those element objects are what you can then call on an instance of the page object. So, as an example, if you were to inspect the username object that gets created for you, you would see it looks something like this:

#<Watir::TextField:0x..fa0bd8f12 located=false
  selector={:id=>"username",
	:tag_name=>"input or textarea",
	:type=>"(any text type)"}>

This is what proxies the friendly name :username (of text_field type) to the underlying Watir implementation (of the Watir::TextField type). This means you can call all methods that are relevant to a Watir::TextField on the :username object.

This is part of how Symbiont tries to let you write concise and expressive tests, by essentially letting you interact with the underlying library via your higher-level domain objects.

Page Object Design

Let me briefly digress here to talk about the design approach.

What you are seeing with the page definitions are that they can be configured using macro-style method invocations. The idea is to have a highly readable block of code at the top of the class that makes it immediately clear how that class is configured. These methods declaratively tell Symbiont how to manage pages by essentially stating what can happen on or to those pages.

Many of the invocations do some amount of metaprogramming, meaning that they participate in adding behavior to the class at runtime, in the form of additional instance variables and class methods. But as you saw above, these declarative methods are really just regular method calls. They are method calls that execute in the context of the class object. The method argument parentheses are usually left off to emphasize the declarative intention.

But why does any of this matter? Well, first I think it makes for a very concise way to define a page object. But you might also note that you can have anyone of pretty much any skill write a page definition for you and not require them to know a ton of Ruby. This is the nice thing about a language that “gets out of your way” and does not force you to use a lot of programmatic boilerplate.

Delegating Actions to the Page

Now let’s consider those specific statements again:

From a pure scripting perspective, that’s not too bad, yet each statement is providing some low level details in the script. For example, you specifically “click” two things and “set” two other things. Further, in this case it’s probably clear that the statements are logging in to something but you can certainly imagine other logic wherein the intent of the action is hidden by the implementation details. Also, if you changed the friendly names, the above statements would have to change. That doesn’t become a problem until you start re-using those statements in different execution contexts.

I think what most automation script writers would do, upon seeing the code above, is decide to wrap it in a method. And that’s pretty much exactly what you would do with Symbiont. Let’s say I want the primary portion of my script to look like this:

Regarding that last line, clearly I’m calling a login_as method and I’m passing a hash that contains the username and password as arguments. Why a hash? Why not just a string? I could have done that as well. That’s the point: here you’re just using the same coding techniques you would use for any Ruby logic. You can probably see that to provide the page.login_as functionality that I want, I just have to provide a login_as method on my page definition that handles the actions I want. Here’s an example:

The login_as method takes an argument and then checks if it’s a hash, which is what allows my call to it to work. Notice here that I’m calling the username and password element methods just as I did outside of the page definition. Further note that I’m calling another method (verify_successful_login). There is no exception handling in the method and I’m doing that just to keep things simple. In fact, such logic might be what you further delegate to helper methods. I’m not going to go into that territory for now but just know that helper modules could be used to further refine how much information is in the page definition.

Page Objects Provide the Context

Since pages provide the context for a web site, just as activities would for a web service, it seems logical to separate them out from the scripts. Numerous scripts, after all, may refer to the same page definitions. To play along and see how this works, in a working directory create a file called pages.rb and put the page definition from above in it:

In a file called script.rb, put just the logic of your script with the addition of a require line for the pages.rb file:

Now let’s try automating another portion of the Symbiote site. There is a “Weight on Other Planets” portion of the site, located at http://localhost:9292/weight. This page cannot be viewed unless you are logged in.

Exercise: Before reading on, take some time to navigate to the page and see how it works. There’s not much to it so it won’t take you long. When you have done that, try to construct a page definition based on the knowledge you have. Put that page definition in the pages.rb file.

Did you give it a shot? Here’s one way to do it:

Something interesting to note here: I do not have a url_is assertion. Having that would allow me to easily just “go to” the page. Yet what that also allows me to do is not use the site navigation to do so. Is that a bad thing? It really depends. Some testers feel that scripts should always use the site navigation at all times. Others feel that you should definitely test navigation in a set of navigation scripts, but then, past that point, you can use convenience actions, like going directly to a url. In the end, do whatever you want to do as long as you are getting the test coverage you need.

Here I’m going to use the site navigation to showcase something about page definitions. Specifically, a page definition does not necessarily have to involve a whole page. It can refer to just part of a page. Consider the navigation pull-out that the Symbiote page provides. That might be a page definition in itself: it just so happens to be a partial page definition.

Exercise: Before reading on, try to make a page definition for the navigation functionality of the Symbiote site and put it in pages.rb.

Did you try it? Here’s one possible way to do it:

So now we have a few page definitions. You already know how to login, so …

Exercise: Given what you know, try to write a script that reuses the login functionality to log in, navigates to the Weight on Other Planets page, and then puts in a weight value of 200 and calculates based on that value.

What did you come up with? Here’s one possible solution that would work:

Here I essentially just keep instantiating a new page definition as my script gets to that part of the flow where the page is going to be interacted with.

Now — key question here — assuming you did something like the above, did you get an error? It’s quite possible you got an error like this:

[remote server] Element is not currently visible and so may not be
interacted with (Selenium::WebDriver::Error::ElementNotVisibleError)

That’s coming up because of the call in line 17. The problem is that Watir may move a little quicker than the display. In this case, Watir is likely trying to click the link before the paragraph pull out actually was pulled out. That would, of course, mean it was trying to click on an element that wasn’t visible yet. This is where being able to harness Watir-WebDriver directly on your elements can help you out. All you have to do is tell Watir to wait until the element is present. So you could change line 17 to this:

Notice, too, how the method chaining fluent interface allows your logic to remain concise and expressive.

As this point, your script should be working. So let’s clean it up even further.

Exercise: Use methods to put the logic of the tests in the page definitions themselves.

Clearly there are numerous ways you can do this. First, here’s what I did in my script:

To support this I had the following logic in the respective page definitions:

Notice how my navigate_to method relies on a bit of dereferencing. Whether or not that’s a good way to do it, the point is once again that you can do whatever you want. This allows you to expose an API of sorts. For example, my navigate_to method could take various types of parameters and act accordingly. Likewise, the same could occur with my login_as method, where I could support taking a hash or a string or even an object. How page definition actions can be called can be documented as part of your API.

Going back to the script, what makes all this work is the fact that you establish a new context — an instance of a page definition. At that point, the context is being stored in the page variable. Let’s look at the script that I came up with in full:

I think that’s pretty concise and mostly intent-revealing, even at the code level that we are operating at. The test script is stating what to do but the details of how have been delegated to the page definitions. As concise as that is, Symbiont does provide a mechanism that allows you to streamline that a bit more. I’ll talk about that in the next post.

Share

About 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.
This entry was posted in Automation, Symbiont. Bookmark the permalink.

2 Responses to Symbiont and the Page Object

  1. I’ve written about DRY page objects in my blog post “Keep Your Page Objects DRY”.

    • Jeff Nyman says:

      Good stuff. Thanks for sharing. It’s definitely one of the most effective patterns I’ve found for automation, the others being context factory and data builder.

Leave a Reply

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