Lately I have been developing lots of test solutions, most of them having to do with automated testing. I originally developed a library that I called Lucid. This was in turn based on a library I was originally calling Terminus. Now I’m trying to distill a lot of what have learned into a library that I’m calling Symbiont.
I’ll be perfectly honest: what I’m writing here is largely for my own benefit as I get my thoughts in order, but I do have hopes for Symbiont to be something useful. So I will document its development path here and if someone besides me finds anything of interest in it, that’s great.
These days, if I can help it, I avoid the cost solution tools like SilkTest or QTP. I instead focus on utilizing the open source libaries that are out there. Open source libraries, however, are often a mixed bag of features and abilities. They can be a lot of work in terms of getting exactly what you want out of them.
The most common ones you’ll hear about these days are most likely Capybara, Selenium, and Watir. I had done some spiking with these tools in the past. As it turned out, there were certain things I liked about Selenium and there were certain things I liked about Watir. I also liked Capybara but it’s largely just a DSL for Selenium and had no interaction at all with Watir.
So what I want to do with Symbiont is have it act as a façade over a set of existing libraries. The reason for this is it will allow me to normalize interactions with those libraries while also allowing me to utilize the parts of each that I like, avoid the parts I don’t like, and ultimately unite them under a common library. What I want to show here is what Symbiont is trying to evolve towards.
For any automated test design, you will define a test harness that utilizes Symbiont. To actually perform the automated testing, you will have to use a driver library. As I indicated, currently Symbiont is targeting Selenium and Watir and specifically those variants that use the WebDriver API. Watir-WebDriver uses Selenium-WebDriver behind the scenes so these two seem like logical libraries to combine even further with a consistent interface.
Symbiont will utilize these libraries automatically for you. What this means is your test harness DOES NOT have to include statements like these:
1 2 |
require 'selenium-webdriver' require 'watir-webdriver' |
Symbiont will automatically make those available for you if you choose to use them. The only thing you have to do is this:
1 |
require 'symbiont' |
You’ll put that in every test script or in a common file that all test scripts will reference. You will then create a “browser instance” with whatever library you want to use. If you want to use Watir, you’d do this:
1 |
@browser = Watir::Browser.new(:firefox) |
If you want use Selenium, you’d do this:
1 |
@browser = Selenium::WebDriver.for(:firefox) |
These statements create a browser instance that is based on a particular browser driver running against a particular browser. In this case, I’m using Firefox but you can use other browsers. Any given test script would only be able to specify one of these drivers to operate in a given context. A test harness, on the other hand, could determine which browser instance to create based on configuration parameters. Here’s one example of what you might do:
1 2 |
@browser = Watir::Browser.new(ENV['BROWSER']) if ENV['DRIVER'] == 'watir' @browser = Selenium::WebDriver.for(ENV['BROWSER'].to_sym) if ENV['DRIVER'] == 'selenium' |
For those curious, these browser instances look like this behind the scenes:
#<Watir::Browser:0x6012b06e> #<Selenium::WebDriver::Driver:0x56194eba browser=:firefox>
You’ll notice there that Watir and Selenium establish different kinds of browser instances, which is probably not terribly surprising. The key thing to note here is that the browser instance is going to be associated with a driver. What Symbiont does is create a platform object and these are the key to executing against a given browser with a given test API. So the browser + driver are a platform to Symbiont. If you’re curious, behind the scenes these objects look like this:
#<Symbiont::Platforms::WatirWebDriver::PlatformObject:0x2cbe8a0> #<Symbiont::Platforms::SeleniumWebDriver::PlatformObject:0x2cb9608>
Now, so far, all of this has very little do with Symbiont. All of this, in fact, is what you would do if you were just using Selenium or Watir by themselves.
The operating logic of Symbiont is going to be “a definition using (or associated with) a browser driver.” What does that mean? Symbiont will exist as a module that is incorporated into page definitions or activity definitions.
“Page definitions” are essentially going to be classes that provide the template for the web objects that exist on a given web page. The page definition is turned into a page object by Symbiont. “Activity definitions” are classes that not only provide the template for web objects on a page but also actions that take place within the context of that page using those web objects.
When “page” and “activity” definitions work together, you have a workflow definition. Put another way, the goal is going to be to allow you to compose activities to make up workflows.
Okay, so I mentioned that the operating logic is “a definition using (or associated with) a browser driver.” You will have a page definition (a class) that represents a facet of the application that can be interacted with. A “facet” here can refer to a complete web page or just part of a page or even an abstract concept, like a workflow. Whatever that class represents, it will incorporate the Symbiont framework. So as a very simple example:
1 2 3 4 5 6 7 |
class LoginPage include Symbiont end class PlanCreate include Symbiont end |
Here I have defined a page definition (or page class, if you prefer) as well as an activity definition. You’ll notice that, in reality, the distinction doesn’t make a difference. It will make a difference once Symbiont is more fully developed. Both definitions have indicated that they will incorporate the Symbiont library by including it. What this means is that when any test scripts want to work with this page or activity definition, a specific “page instance” or “activity instance” will be created and associated with the browser instance (and thus the browser driver). And remember the browser and the driver are wrapped up in a platform object. The platform is what Symbiont uses as its interface to test scripts.
What you end up with is a situation where “web objects on a given page are used by Selenium running against Firefox” or “web objects during a certain activity are used by Watir running against Internet Explorer”.
What does “used by” mean there? It means those objects are being referenced or accessed by Selenium or Watir. But what’s telling Selenium or Watir how to reference or access those objects? Well, ultimately this will be your test script. And your test script — which says what to do — is delegating the responsibility of how to do it to the Symbiont library. Or, at least, that’s the plan as Symbiont evolves.
So let’s consider two scripts here — one in Watir and one in Selenium. These will show you what Symbiont can do now and will also serve as a roadmap for what Symbiont has to handle in the future. These scripts are written against my sample Sinatra web application. If you actually want to try running this, you will need to install the Symbiont gem. Just do this:
gem install symbiont
First the Watir script:
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 |
require 'symbiont' class Login include Symbiont url_is "http://localhost:4567" end @browser = Watir::Browser.new(:firefox) action = Login.new(@browser) action.view @browser.frame(id: "loginSection").text_field(id: "clientCode").set "QA" @browser.frame(id: "loginSection").text_field(id: "loginName").set "administrator" @browser.frame(id: "loginSection").text_field(id: "password").set "password" @browser.frame(id: "loginSection").button(id: "btnSubmit").click @browser.link(id: "testPage").click @browser.text_field(id: "title").when_present.set("Revelation Space") @browser.select_list(id: "concepts").select("Tachyonic Antitravel") @browser.checkbox(id: "oc").set @browser.checkbox(id: "harpoon").set @browser.radio(id: "mtc").set table = @browser.table(id: "atomic").when_present puts "Test Passed: Atomic Table found" if table puts "Table with id=atomic:\n" + table.text table.rows.each do |row| puts "Cell Value: " + row.text end @browser.close |
Now let’s look at the Selenium version:
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 |
require 'symbiont' class Login include Symbiont url_is "http://localhost:4567" end @browser = Selenium::WebDriver.for(:firefox) action = Login.new(@browser) action.view wait = Selenium::WebDriver::Wait.new(:timeout => 10) @browser.switch_to.frame("loginSection") @browser.find_element(id: "clientCode").send_keys("QA") @browser.find_element(id: "loginName").send_keys("administrator") @browser.find_element(id: "password").send_keys("password") @browser.find_element(id: "btnSubmit").click @browser.switch_to.default_content @browser.find_element(id: "testPage").click element = wait.until { @browser.find_element(id: "title") } element.send_keys("Revelation Space") list = @browser.find_element(id: "concepts") options = list.find_elements(tag_name: "option") options.each do |item| if item.attribute('text').eql? "Tachyonic Antitravel" item.click end end @browser.find_element(id: "oc").click @browser.find_element(id: "harpoon").click @browser.find_element(id: "mtc").click table = wait.until { element = @browser.find_element(id: "atomic") element if element.displayed? } puts "Test Passed: Atomic Table found" if table puts "Table with id=atomic:\n" + table.text @browser.find_elements(xpath: "//table[@id='atomic']/tbody/tr/td").each do |item| puts "Cell Value: " + item.text end @browser.close |
So what actually is Symbiont doing here? Well, not a whole lot. In fact, the only specific functionality that Symbiont is exposing is this (in both scripts):
1 2 3 4 5 6 7 8 |
class Login include Symbiont url_is "http://localhost:4567" end ... action.view |
The call to the view() method is provided by Symbiont. The reason it works is because of the url_is() method, also provided by Symbiont. What this shows you is that Symbiont has exposed a way for you to provide what the URL is for a give page or activity definition. If such a URL has been established, then a call to view() will essentially be a call to that URL.
That’s not a whole lot to show for a library, is it? Well, Symbiont is young and is in development. However, what the above scripts show you is what I want Symbiont to handle. While there are some obvious similarities in the above scripts, there are also clearly some major differences. What I want Symbiont to do is smooth over those differences, enhance the similarities, and provide a common interface to both library APIs.
So, as Symbiont evolves, the above scripts should not only merge, but should actually start to look quite a bit different yet still maintaining the basic functionality.
It’s a fun challenge and I’ll see where it goes!
I like the idea of creating a “facade” over Selenium and Watir that would allow a test engineer to pick and choose the best framework for the portion of the application under test. But to play Devil’s advocate, would it be easier to maintain a Watir framework and Selenium framework rather than try to mash the two together into a Sybiont?
You may not know the answer to this questions yet, but I am curious to see what you discover.
There actually already is a project that does something similar called page-object. In fact, I want to take something like that project in a slightly different direction. Would it be easier to just pick one and use it? Short answer: Yes. As an example of someone who chose just that path, check out Alister Scott’s watir-page-helper, where he focused solely on Watir.
There are, however, times where I really would like to use Selenium since it tends to be a lower-level API and other times I’d like to use Watir for its simplicity, such as when dealing with tables. Also, Selenium and Watir both have certain selectors they recognize that the other library does not. What I’d like to do is normalize that a bit so that you can use whatever selector you want, regardless of the library.
That being said, I’d be lying if I said I was certain this wasn’t a complete waste of time.