Along Came a Symbiont, Part 1

I’ve talked about my automated testing framework Symbiont in a variety of posts. Because it’s been used in a variety of locations, I’m going to use this post to do a gentle introduction to the framework in a slightly different way than I have previously.

Incidentally, the title of this post is based on the comic “Venom: Along Came a Spider” released in January of 1996. If you’re curious why that has any relevance at all, the GitHub page for the project will clue you in.

Symbiont is designed to place a thin wrapper around Watir-WebDriver. Watir is a library that automates actions within a browser. Watir-WebDriver does the same thing but uses the WebDriver API. Both libraries are in turn a wrapper for Selenium-WebDriver.

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 symbiont
$ gem install rspec

Installing Symbiont will automatically cause Watir-WebDriver to be installed, which in turn will make sure that Selenium-WebDriver 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.

Is Symbiont a Framework?

Well, I call it a micro-framework and I’ll just quote myself from the project’s home page:

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.

Okay, But Does That Matter?

Realistically, that depends. My view is that an efficient 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. So let me just talk about a few of my goals and why they mattered to me.

My goal with Symbiont is simplifying syntax and making each line of code easier to comprehend. This is important to me because Symbiont is used in the context of automated checking, and automated checking 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 is also much less chance of accruing technical debt with this kind of approach.

All of this mattered to me in the creation of Symbiont 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 is actually being checked. So, essentially, the idea is a certain linguistic sophistication in the pursuit of concise programming.

One last point is that Symbiont was designed around the idea that the test runner, the test library, and the tests themselves are entirely separate. This means you can change one without the other. In fact, a measure of progress is that you are changing your test library less and less.

Symbiont Uses Watir

Create script.rb. First we’ll start with a simple Watir script:

Very simple stuff. It just starts up a browser (defaulting to Firefox), situates the window in terms of placement and size, and then goes to a particular URL. Once that’s done a few expectations are checked and then the browser is closed.

Now let’s talk about Symbiont. We’ll apply the same script with some modifications:

So what’s going on here? All this really got you is more typing. You essentially have to preface the previous commands with “Symbiont”. Instead of requiring “watir-webdriver” I now require “symbiont”. Symbiont, in turn, requires watir-webdriver for you. Likewise, the previous direct call to instantiating Watir::Browser is now handled by the call to set_browser. I left the original commands commented out so you could see the difference.

Firefox is the default browser driver included with Selenium which is why the scripts used so far automatically call up Firefox. I could have made this more explicit in the original script or the Symbiont script like this:

With the original script you could have Watir run using a different browser like this:

You can do the same thing with Symbiont:

This does, of course, require that you have the appropriate browsers and drivers available.

Symbiont and the Underlying Drivers

With the Symbiont script, there are a few expectations you can use to check things out. For example, if you put the following expectations anywhere after line 8 and before the last line you’ll find that they pass:

What these expectations are telling you is that Symbiont.browser is wrapping Watir::Browser, which is how watir-webdriver is recognized. Further, if you call the driver instance on Symbiont.browser, you’ll find that it’s wrapping Selenium::WebDriver, which is how selenium-webdriver is recognized.

This, by the way, is exactly why I think the page-object gem is way overcomplicated for what it tries to do. It has far too much internal complexity because it supports Watir and Selenium directly … but separately. This is as opposed to simply relying on the fact that Watir delegates down to Selenium. This has led to what I consider a very inelegant code base and was one of the reasons I developed Symbiont rather than use the page-object gem.

Incidentally, if you want to check what particular versions of the underlying driver tools are being used you can check:

You’ll get something like this:

Symbiont v1.2.0
Watir-WebDriver: 0.9.1
Selenium-WebDriver: 2.52.0
Capybara: 2.6.2

Notice that Capybara is in that list. Capybara is another library, like Watir, that wraps Selenium. Symbiont also wraps Capybara. Right now my support for Capybara, while certainly present (see this post), this is not an area I’ve been putting as much emphasis on recently.

To further reinforce all this, let’s get a list of what the browser exposes for your use:

This lets you see all the methods that Watir provides. Now you can try this:

This shows you the methods that Selenium is providing.

So what you’re seeing here is that, from a certain perspective, using Symbiont is not necessarily giving you much. It’s just a layer of indirection around Watir and Selenium … and thus around WebDriver. So what’s the value-add here?

Page Definitions

Symbiont provides a convenience mechanism for automated checking with WebDriver … provided that you are willing to describe your application in terms of activity and page definitions. Page and activity definitions 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. They could also refer to a service. (I’ve been experimenting with service objects in Symbiont, but I have yet to document too much about this as I determine if this worth it.)

In any event, with Symbiont, those definitions are proxied to Watir-WebDriver so that they can be used as the basis for executing automated checks against a web browser or web service.

So let’s consider what that looks like:

The addition here is the class called Decohere. Decohere is nothing more than a Ruby class. I could have named this class anything I wanted. Since it’s the home page of the application, I could have, for example, simply named it Home. What makes this class a page definition is that Symbiont is attached to it. Note that attach is basically a synonym that Symbiont provides for the Ruby include action. Attaching the Symbiont means that the Decohere class is treated as a special kind of class with some extra functionality. So essentially what you do is turn a generic Ruby class into a Symbiont-specific class.

Let’s check out a few things here:

What this shows you is that the page instance is a kind of Symbiont, and that makes sense because remember that Symbiont is being attached to the page. But the page instance itself is an instance of Decohere, which is the class of the page definition.

Let’s inspect what we’re dealing with.

These will provide the following identical output:

#<Watir::Browser:0x..f82cbcc9b2450c6c6 url="https://decohere.herokuapp.com/stardate" title="Decohere - Stardate Calculator">

#<Watir::Browser:0x..f82cbcc9b2450c6c6 url="https://decohere.herokuapp.com/stardate" title="Decohere - Stardate Calculator">

This shows you that the browser object for Symbiont and the browser object for the page are one and the same. So Watir-WebDriver has been proxied to the page definition.

That will give you this:

#<Decohere:0x007faecaad74a8 @browser=#<Watir::Browser:0x..f82cbcc9b2450c6c6 url="https://decohere.herokuapp.com/stardate" title="Decohere - Stardate Calculator">>

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.

Browsers and Pages

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 Symbiont. This is important in case you have other test frameworks that similarly create a @browser instance variable. In other words, this is what allows Symbiont to play nicely with other tools you may be using. Details are encapsulated in the Symbiont 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 Symbiont controlled page object will be wrapped around the driver.

Let’s investigate our page a bit more. Try this:

The following will tell you the above along with everything that Symbiont provides to a page instance. This is effectively giving you some insight into the Symbiont API, which is attached to a page definition. If you remember earlier, I had you try Symbiont.browser.methods, which showed you the Watir WebDriver API.

Using the Page Definition

Right now I have code like this:

However, that seems a bit inefficient. If I have many page definitions that become page objects, this means my script has to know the URL and visit it each time. But that means a lot of duplication over my automated check logic. It would be better if the page itself knew how to go to the appropriate URL. The automated scripts could then just delegate to the page. Well, that’s certainly possible to do.

Each page definition can have declarations on it. These declarations will describe the contents of the page or aspects of the page. For this example, you can add a declaration for the URL:

Notice that the call to visit (which required the URL) is changed to a call to view (which does not require a URL). The url_is declaration allows the page to indicate what its URL is. In fact, I was already checking page.url in one of my expectations. But I could also check that the actual URL that this action reports matches the asserted URL on the page definition.

Now let’s do something similar for the title. Originally I was checking page.title in one of my expectations. But I could also check that the actual title that this action reports matches an asserted title on the page definition. So let’s add the assertion and the check. First the assertion:

Now I can do something similar with the title that I did with the url:

There is a slightly nicer way to have the title checked. There is a has_correct_title? method that can be checked for a true or false value. If this method returns true, that means the page displayed in the browser was found to have the asserted title. Here are some examples of statements you could use:

The last statement there shows an example of how RSpec lets you be flexible with predicate methods (i.e., methods that end with a question mark).

You might wonder if you can do the same thing with the URL. You can but the URL requires an additional declaration to the page definition. The reason for this is because the url_is assertion must be exact because it’s used to actually navigate to the page. But sometimes navigating to a page takes you to another page or adds parameters to the URL. In those cases, you may want to match on that specific final URL. So you can add another assertion to your page definition:

With this in place, you can do similar checks for the url that you did for the title, as such:

The url_matches assertion can be any regular expression. So, for a simple but probably unrealistic example, if you want to make sure that your page was always referencing a four digit port numbers, you could do something like this:

As a side note, you can check both of these states — title and url — with a single action like this (the second being another use of the RSpec predicate approach):

In this case, verified? means checking both the title and URL. You might have cases where you don’t want to check the title at all. In that case, you can just use displayed?:

So to make sure this is clear: if you want to use a verified approach, you must provide the url_matches and title_is assertion definitions on your page definition. If you want to use a displayed approach, you only need to provide a url_matches assertion definition.

Here I’ve covered some of the high-level details of Symbiont, aiming to prove that it is a viable solution in terms of how it wraps two popular driver libraries and how it forces adherence to an effective pattern (the page object pattern). I’ve shown you here some of the API that Symbiont exposes and you can check the internal API page of the Symbiont wiki for more information.

In the next post I’ll cover more usage of the page definition and different ways to express script logic.

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.

Leave a Reply

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