As well as having the tests clear of HTML knowledge, I like to keep the PageObject classes as simple as possible when I can. WebDriver is very handy and succinct, but I like to go even further, so instead of my PageObject containing:
driver.findElement(By.id("submit")).clickI would much prefer to just write:
click(id("submit"))I already have an abstract base class called
Page
, so the solution is simple enough: write a click() function in the base class and somehow import all the methods in org.openqa.selenium.By
.Being super-lazy, I don't really want to have to add
import org.openqa.selenium.By._
to the top of every Page
subclass, especially seeing as I know I'll need them in EVERY subclass. I want the Page
to provide this importing for me, so that the methods in By
are automatically available in every subclass.Failed Attempts
I tried a couple of solutions before I found what I believe is the laziest working approach. I tried just importing
By._
in the Page
class. This seemed too simple to work, and I was right. I also tried to import the methods with identical aliases (import By.{id => id}
) but this didn't do anything either.I thought maybe I could extend the
By
class and bring all the methods into the Page
namespace that way, but the By
class is both a factory for By
instances as well as the abstract interface for those classes themselves, so I can't extend it without implementing the abstract findElements()
method, which doesn't really make sense.I concluded that explicit delegation was the only solution, so I resorted to writing this:
def id(id: String) = By.id(id)That didn't seem too bad, but by the time I was half way through writing the second one, I decided that this was not lazy enough. I didn't want to have to retype the signature of all these functions.
A Suitably Lazy Solution
And then I remembered that I was working in a functional language, and that the functions I was delegating to were actually first-class values in Scala. So all I really needed was a way to provide an alias for those function values that allowed my subclasses to invoke the alias. The solution I came up with is to partially-apply each of the functions with no arguments and to make my alias functions return these partially-applied (though in reality completely unapplied) functions.
The resulting code looks like this:
def id = By.id _Now my subclasses can use the functions
def linkText = By.linkText _
def name = By.name _
def xpath = By.xpath _
id()
, linkText()
, name()
and xpath()
without qualification, and I didn't have to re-type the signatures of each one. It obviously didn't save me amazing wads of time in this case (especially now that I've written a blog entry about it) but in some other scenario where there are many methods with non-trivial signatures that need to be delegated, this could be a real time-saver.What Does it Cost?
Note that there is a slight cost to what I've done here. Scala doesn't see what I've done and have its compiler automatically substitute the straightforward delegating method that I originally wrote. Instead, scalac creates a new class, a subclass of
FunctionN
, for each of these methods, with the classes having names like Page$$anonfun$id$1
. The implementation of the Page.id()
function that Scala outputs will create and return an object of this class, such that the caller then executes apply()
on the Function, which in turn invokes the original method on By
(statically at that point - no reflection). So my desire for laziness at the source level has resulted in some indirection at runtime, but this kind of simple functor creation is unlikely to cause any serious performance pain.(If you were really worried about it, you could make the aliases
val
s instead of def
s so that the functor objects would only be instantiated once, but then IDEA will start highlighting the usages of the aliases as fields instead of methods, which just looks weird.)If you want to know more about the gory details of Partially-Applied functions, I would suggest grabbing a copy of "Programming in Scala" from O'Reilly from the Book Depository:
Programming Scala - Dean Wampler & Alex Payne (O'Reilly)
or from Amazon:
You can also do
ReplyDeletedef id = () => By.id
which is basically identical to
def id = By.id _
seems to be a matter of style. The first style is "closuresesque" and the second is "partially applied" style.
-- Eric
My bad - I didn't realize id takes parameters.
ReplyDeleteIt should have been def id = (x: String) = >By.id(x)
The partially applied style is definitely cleaner.
My understanding is that they both are interpreted as
new Function1[String, By] { def apply(x: String) = By.id(x) }
ps - I just made those style names up to be silly... thx for your post.
Thanks, Eric. I did try your suggestion and I couldn't get it to work, but I figured you must have just known something I didn't!
ReplyDelete