I keep looking for ways to get junior testers up to speed on writing test frameworks and test libraries. It’s a very good skill to have and it’s one that’s in demand. Beyond that, I think it paves the way for helping testing to evolve into what it must become: a group of practitioners that are test solution developers. As such, I’ve been using my Symbiont library for this purpose.
I always like to note up front that what I’m doing isn’t all that special or even all that unique. My library is actually doing what a lot of other libraries do. What is different is that I’m working to document it’s creation a bit as well as some of the decisions I made along the way. (See the posts regarding the start of the project and the continued evolution of the project.)
For example, one major decision with the latest incarnation is that I dropped Selenium entirely and went solely with Watir for now. (See the articles tale of three testing APIs and Selenium WebDriver vs Watir-WebDriver.) If I ever do reintroduce Selenium — which I think has a cluttered and ungainly API — I will do so via Capybara, which puts a nice wrapping around Selenium to smooth out its many rough edges.
Another thing I’m trying to do is promote the idea of humanizing interfaces. Too many of these frameworks are written in such a way that you really have to “think like a developer” to even use them. I think that’s ineffective, even if you ARE a developer. And, in fact, that very dichotomy is what I think is shown no better than in how you use the Watir API versus how you use the Selenium API. Selenium feels like it was written by developers. Watir feels like it was written by testers. (And, strangely enough, that’s all pretty much the case.)
So what has developing Symbiont allowed me to show other testers? Well, it’s a project that I had to write the requirements for. But I did so in the form of tests. I have code-based tests (Rspec) and I have acceptance tests (Cucumber). It’s a project that I wanted to make available to any testers who could set up a Ruby environment, so I distribute it as a gem and make it available via GitHub. Because of this, I had to make sure that the tool looks somewhat professional, is well commented, and is under strict version control. Most importantly, however, this gets testers thinking like test solution developers and test solution providers, both (or either) of which are the future of what “being a tester” means.
What I want to do here is start talking about the construction of a humanizing interface to such tools. You’ll see only the barest beginning of this here. First, you have to have a Ruby environment installed. Then make sure you install the Symbiont gem:
gem install symbiont
Now let’s try creating a script. Create a file called learn_symbiont.rb. The most basic way to start is to include the Symbiont library right away:
1 |
require 'symbiont' |
The library expects a browser instance to be availble to it:
1 |
@browser = Watir::Browser.new(:firefox) |
As part of my testing of Symbiont, I use my small testing application. Since we’ll use my small application, the first thing to start with is the login page. You can define a page definition for this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class LoginPage include Symbiont url_is "http://localhost:4567" title_is "Login | Test App" look_for :static_test_page link :static_test_page, id: "testPage" within_frame(id: "loginSection") do |frame| text_field :customer, id: "clientCode", frame: frame text_field :username, id: "loginName", frame: frame text_field :password, id: "password", frame: frame button :login, id: "btnSubmit", frame: frame end end |
While I call this a page definition, it’s really nothing more than a traditional page object. However, I like humanizing interfaces and the start of this is using terms that are somewhat indicative of intent. The above would actually be a page class that serves as the basis for page objects. That’s what it does. However, what it is could be referred to as a definition for a page. It contains declarations … as in “I declare that a link that I want to call static_test_page exists on the page, and you can find it by looking for an id of testPage.” Or “There is a frame on the page called loginSection and within that frame you will find a text field that I want to call username but that you will identify with the id loginName.”
There are also some assertions. Those are shown by the url_is, title_is, and look_for. These assertions let you do certain things like:
1 2 3 |
action.view # <-- uses url_is action.has_title? # <-- uses title_is action.has_object? # <-- uses look_for |
Actually, in typing this out I realize I don’t like “has_object” as that seems to be a bit too developer focused. I may have to change that. In any event, to see these assertion statements fail, change the title_is text or the look_for text in the LoginPage definition. That should cause the script to fail.
As far as the declarations, those let you reference and manipulate the web object.
So let’s consider what this means for the link object that is declared. Since I have action context, I can use that to do various things:
1 2 3 4 5 |
puts "\t'Test Page' link object exists? #{action.static_test_page_exists?}" puts "\t'Test Page' link object exists? #{action.static_test_page?}" puts "\t'Test Page' link object visible? #{action.static_test_page_visible?}" puts "\t'Test Page' link object visible? #{action.static_test_page_?}" puts "\t'Test Page' link object text is: #{action.static_test_page_text}" |
I can also get a reference to the web object itself. What this allows me to do is talk to the object via Watir.
1 |
test_page = action.static_test_page_object |
Here I’ve suffixed the name with the text “_object.” I could have also used the text “_link” since, in this case, the object is a link. If it were a button, I could have suffixed it with “_button” and so on.
What all this lets me do is use the object directly:
1 2 3 4 |
puts "\t'Test Page' link object is type: #{test_page}" puts "\t'Test Page' link object text is: #{test_page.text}" puts "\t'Test Page' link object text is: #{test_page.visible?}" puts "\t'Test Page' link object text is: #{test_page.exists?}" |
So if I can do the latter, why have the former? In other words, if I can reference the object directly and call methods like visible?, exists?, and text on it, then why have my own methods that get suffixed to the methods (like “_visible?”, “_exists?”, and “_text”)?
Well, it’s in the nature of the Symbiont! The Symbiont attaches to Watir just as your script attaches to the Symbiont. The former approach allows me to do different things or override some aspects of behavior but ultimately still drop down to the level of the browser driver.
I can also click the link. With the former approach, I can do this:
1 |
action.static_test_page |
With the latter approach:
1 |
test_page.click |
With the former approach, let’s consider how this is working. You declared a web object and then you were able to call certain methods on that web object. So, specifically, the very act of creating this declaration …
link :static_test_page, id: "testPage"
… creates, behind the scenes, a series of methods like this:
static_test_page_exists? static_test_page? static_test_page_visible? static_test_page_? static_test_page_text static_test_page
The last method in the list does a default action, which in the case of links, corresponds to clicking the link. The other methods are used to check properties of the link, such as whether it exists, is visible, and what it’s specific text is. Notice too that “_exists?” and “?” are aliases of each other as are “_visible?” and “_?”.
Right now Symbiont supports links, buttons, and text fields. As you can imagine, what you see for links works pretty much the same for text fields and buttons. I leave verifying this as an exercise for the reader. (Hint: You can download the Symbiont code base from Github and check out the Cucumber tests in the specs directory to see how things work.)
Now let’s look at another common pattern to these kinds of libraries, which is a convenient way to utilize page objects. How I like to word it is that you establish an context for a set of actions on a page or as part of a workflow. So let’s say I have this page definition:
1 2 3 4 5 |
class LoginPage include Symbiont url_is "http://localhost:4567" end |
Let’s also say I have this activity definition:
1 2 3 4 5 6 7 8 9 10 11 |
class LoggingIn include Symbiont begin_at "http://localhost:4567" within_frame(id: "loginSection") do |frame| text_field :clientCode, id: "clientCode", frame: frame text_field :loginName, id: "loginName", frame: frame text_field :loginPassword, id: "password", frame: frame button :login, id: "btnSubmit", frame: frame end end |
They look quite a bit alike don’t they? Realistically, they are. They differ in their name — LoginPage versus LoggingIn — and some of their assertions. The LoginPage definition has url_is while the LoggingIn definition has begin_at. If I want to use the page definition, I can set my action context:
1 2 |
action = LoginPage.new(@browser) action.view |
On the other hand, if I wanted to use the activity instead, I could do:
1 2 |
action = LoggingIn.new(@browser) action.start |
Would I really have two definitions like this? Probably not, actually. But it’s illustrative for now. What I’m trying to do is utilize a humanizing interface where you can word things the way you would like based on what is actually going on with the logic. That’s why a page definition indicates that its “url_is” something and an activity definition indicates that it will “begin_at” a specific URL. Behind the scenes, however, the exact same stuff is happening.
If you are curious, behind the scenes, both view() and start() call a platform method called visit(). This visit() method calls out to Watir which uses a goto() method on the browser object.
Now let’s say we want to include a factory that makes setting all this context a little easier. First, let’s start with the same definitions but make sure to include the factory:
1 |
include Symbiont::Factory |
Now I can establish the context for a set of actions:
1 2 3 4 5 6 7 |
on_view LoginPage on LoggingIn do |action| action.clientCode = "QA" action.loginName = "jnyman" action.loginPassword = "password" end |
Here the difference is “on_view”, which will set the context and then go to the context. There is also “on”, which will set the context but not go to it. When I say “go to it”, I mean navigate to the page or location. Again, the above is not how I would do things in an actual script. I would either have a page definition or an activity definition and establish the one I want. However, the above is a way that you could do it, if you felt it was more indicative of what was going on.
Incidentally, you could also use “during” as a synonym for “on”. So that last part could be:
1 2 3 4 5 |
during LoggingIn do |action| action.clientCode = "QA" action.loginName = "jnyman" action.loginPassword = "password" end |
Again, the only reason for even allowing that is simply to make the interface to the code a bit more humanizing, in that it “reads better.”
Now, as I mentioned before, I likely wouldn’t have two definitions for the exact same area like this. Although I might. It really depends. But let’s just say I wanted to call up the activity and visit it. So I can’t use on or during. But I could do this:
1 2 3 4 5 |
start_activity LoggingIn do |action| action.clientCode = "QA" action.loginName = "jnyman" action.loginPassword = "password" end |
Here “start_activity” really does the same thing as “on_view”. The difference, once again, is in how it reads but also in how it allows people to utilize phrases that make sense to them.
Obviously Symbiont is in early days here and this post is not meant as documentation of it, but rather as a way to introduce why I’m designing it the way that I am. In the next post on Symbiont, I’ll probably go through a bit of the logic of the library so that testers who want to develop tools can see at least one way of doing this. Keep in mind that this is a GitHub project so you are certainly free to fork it and do your own practice as well.