I have spent the better part of the last five years learning how to design effective and efficient test frameworks and libraries. I may actually be making progress. This post will detail a bit of why and show how the Dialect project has evolved into a radically new life form, albeit one that did exist before.
I’ve gone through tool iterations with names like TestNLP, Natural Testing, Terminus, Fluent, Lucid, Cogent, Dialect, and so on. One of my early tools in this regard was called Symbiont and I had a series of posts (now deprecated as “Symbiont – Previous Generation”) related to this tool. That name has always stuck with me. (And, yes, I keep in mind my own thoughts on “name your tool intelligently”.) Symbiont is now going to see a rebirth as a new tool and it’s partly as a result of my work with Dialect, which I had been covering in some posts here.
Those Dialect posts were an attempt to showcase myself floundering around like a fish out of water as I built up a library. Interestingly, what I found is that I designed some really poor frameworks in my day. The original Symbiont was a mess. The successor to Symbiont, Fluent, was also a mess. It was a much nicer mess in some ways, but still — a mess. Dialect was starting to revisit a lot of my design decisions in those previous tools and I’ve finally come to a library that I think hits most of the right notes, and very few of the bad ones.
Like Dialect, Symbiont is entirely predicated upon Watir-WebDriver. My main reason for this approach was that I simply wanted a wrapper around Selenium-WebDriver, a library that I personally detest using directly. (Although I do admit it has gotten better over time.) I had mentioned in one of my Dialect posts that Capybara was an alternative solution in this arena. Like Watir, Capybara similarly wrapped Selenium. That said, I still stuck with Watir-WebDriver for a variety of reasons, most focused on my familiarity with it.
I’ll have more posts on how to use Symbiont so in this post I’ll just cover some basics regarding how Dialect evolved into this.
Right now the code base for Symbiont is completely in line with Fluent in terms of the basics of being able to use page objects to drive actions. Yet if you take a look at the respective code bases of both tools (Fluent lib vs Symbiont lib), you will see a huge disparity. Symbiont is quite a bit more tight than Fluent with a lot less code bloat.
I was adamant about keeping the factory module and so that exists largely unchanged between the libraries. However, consider how I handle generators in Fluent, which provided the basis for element interaction, and how I did the same thing using an Element module in Symbiont.
The difference could scarcely be more stark. I was likely going to be heading down a similar path in Dialect, although if you read the Dialect Diary posts, you know I was still battling with different approaches.
The main reason for the stark difference here is that in both Fluent and Dialect, I was adhering to the concept of a platform object. The goal was to normalize interaction among different potential libraries — like Capybara, Selenium, Watir-Classic, Watir-WebDriver. That, however, is what led to the bloat of Fluent. To attempt to normalize the differences between libraries (via a platform object) meant I had to have a whole structure devoted to a platform object for a specific library. That also meant I had to create a series of platform web elements for each library I was wrapping. Not only that, but I had to provide a “common” set of actions on web elements that were supported by any library.
To put it bluntly: what a mess!
How is Symbiont getting rid of all this? Well, the main way is avoiding the platform object. That means focusing on one underlying library. In this case, I focus on Watir-WebDriver, which, remember, does give me access to Selenium-WebDriver should I need it or feel masochistic enough to want it. The way this helps me tighten the code can be seen in this bit of logic from the elements.rb file in Symbiont:
@elements = Watir::Container.instance_methods
Symbiont.elements.each do |element|
define_method element do |*signature, &block|
Here you can see that I’m using Watir::Container directly to get the instance methods it recognizes. Those instance methods correspond to the element names (text_field, checkbox, and so on) that I had to painstakingly redefine in the Generators module of Fluent. In the case of Symbiont, after I store those Watir instance methods, I then have Symbiont iterate over them and define a method that corresponds to each element name on a page object. How this actually works will be shown in upcoming posts.
The upshot of this is that you can call familiar methods (.click, .set, .select, and so on) directly on friendly names that you use for these elements in your page objects. This is conceptually similar to what I was doing with Fluent (and probably Dialect) but in those cases you could not call the Watir methods directly on your elements. Instead you had to use “similar” sounding method calls, provided by a bloated DSL that I provided. And that was all because I had to support different platforms, given my design choices. What this meant practically is that for someone using Fluent who also knew Watir-WebDriver or Selenium-WebDriver, they had to now learn an entirely new API just to use tools they already knew how to use. Talk about frustrating! A question I had to ask myself is: “What benefit is Fluent giving someone that justifies hiding the libraries they already know how to use?” The answer was: Not as much as it should.
As an example of what I’m talking about, check out the buttons section of the generators page. Notice there how there is a login object declared. To check if that button exists, you have to know to append an underscore (_) and the predicate word exists?. So you end up with:
How would you do this directly with Watir (and thus with Symbiont)?
But notice that since I was supporting Watir and Selenium (and perhaps other libraries) I could not rely on a direct call to Watir (.exists?) but rather had to create a “fake” method (_exists?) that, behind the scenes, would either call Watir or Selenium or whatever else. Now, I did have a provision for allowing you to access the underlying driver and thus its methods. With the above, you could have done this:
Now you would be calling .exists on the driver, as you are used to doing, but you had to remember to append the _object part to your friendly name for the web element, making the friendly name just a little less friendly.
To drive the point home, check out the Generators page. Look at the examples there for the text field (firstName) and the select list (state) and see how many methods I was generating. In fact, reading that page shows you the complexity I was building in.
In my first post regarding Dialect, I said: “I’m really hoping to develop Dialect in an incremental and visible fashion. Even if this visibility helps no one but myself, I feel it will be worth the effort.” Well, this is the end result — and possibly an interesting case study for those of you who work on your own frameworks. Sometimes you come back to the place you already were and see it through fresh eyes based on the journey you took. This kind of process can seem utterly chaotic to those who are not used to the churn of bringing new solutions into the world or refining existing ones..
I would argue it’s a controlled chaos, though. And therein lies its strength.
Long live the Symbiont.