Selenium Would Be Awesome If I Didn't Have to Design Around It
I've been using Selenium WebDriver for a few years; because, well, there aren't many other options. If you're doing web work and you didn't get to pick your front-end library from a list of new-hotness frameworks, you're probably using Selenium for your system testing. And you should! It's a good tool. There are, however, a couple of design decisions that make me want to attack Selenium with a comically large hammer made of nails and salt.
I should clarify that we're working in the .Net world, specifically with C#. We're using Selenium WebDriver with Chrome (headless and not). If some other version of Selenium fixes this or if there's another tool that does what Selenium does without these issues, then, great - I'd love to hear about it! If it means switching our front end to some specific framework or abandoning certain kinds of tests, then now is the time for you to sit quietly in the back and Tweet about how much you love the sound of your own voice.
The first of the design decisions that make me want to bring the beat-down on my keyboard is how Selenium handles page objects that aren't visible. Let's consider the test case. Let's say I'm working on an application where data is sensitive and certain user's shouldn't be able to see some data. In other words, just about every website worth having tests for. I log in as user A, create some sensitive data, log out, log in as user B, try to view that data, then I want to show that the data is not visible. Easy, right? So, how has Selenium screwed this up? If I ask Selenium to give me that object that should no longer be visible, it doesn't give me the chance to ask if it's displayed or not - Selenium just throws an exception and blows up my whole test.
Some of you might be thinking that Selenium does support this - you just have to ask for all the instances of your element and check that the count is zero. Yes, you can do that, but it's a crap solution. First of all, that means you need to ask for one object two different ways based on what you expect to do with it. If you have helper methods that encapsulate the knowledge of how to get your application objects, now you have to double the number of helper methods to handle the cases.
Assert.IsTrue(CogApplication.GetAdministratorLink().Displayed,
"The admin should be able to see this.");
// Log out, log in as other user
Assert.AreEqual(0, CogApplication.GetAdministratorLinkWhenIDoNotExpectItToComeBackBecauseSeleniumMakesMeWriteCrapMethods(),
"I need a vacation now. Maybe Norway. They have lovely lakes.");
Secondly, even if you don't mind writing twice as many helpers as should have to, you still end up with brittle tests. Let's say that you have more than one developer working on your application (extraordinary situation, I know). On the page about sprockets, Developer Bobby decided that the setup link should not even be an element on the page when it's not there. You test it and it works fine. Now you're on the page about gears and Developer Jan decided that the setup link is on the page, but hidden. Now Selenium returns the page object, but the Displayed property is false. Your test is unhappy, you're unhappy, and Jan doesn't understand why you replaced all letters on their keyboard with spikes.
So, what have we done to design around Selenium's foolhardy decision? We had to start at the web driver.
public IWebDriver GetDriver()
{
return new SafeDriver(GetChromeDriver());
}
The SafeDriver class is a thin wrapper on the Chrome driver (or the PhantomJS driver, back before PhantomJS was wrapped in moth balls and dumped in the sea). It basically calls the core driver for anything it's asked for, except when you ask it for an element.
public IWebElement FindElement(By by)
{
return new SafeElement(this, by);
}
So, SafeDriver's only job is to return SafeElements every time we ask for an element. The real magic is in SafeElement.
public class SafeElement : IWebElement
{
public SafeElement(IWebDriver driver, By by)
{
_driver = driver; // store for later
_by = by;
}
public bool Dispalyed
{
get
{
try
{
return _driver.FindElement(_by).Displayed;
}
catch (NoSuchElementException)
{
return false;
}
}
}
// All other properties call _driver.FindElement(_by)
// and then the method
}
This isn't a complete solution. This only helps out with the 'displayed' issue. However, I'll say that storing off the driver and the 'by' let us do some nifty things later.
Also, this doesn't fix the issue where, somewhere in your chain, you call FindElements. Because FindElments returns a list and lists have a very large interface, I haven't taken the time to try and work around that and use delayed evaluation of the 'by' condition. Writing more specific FindElement conditions has worked out pretty well for us so far (passing indexes into XPath statements, etc).
Wrapping your driver in a SafeDriver isn't a perfect solution; but, at least you don't have to worry about 'in what way' an element is not displayed. It's better than stabbing your monitor. Or shouting at the developers. Well, I can't promise you won't still do those things. There are other terrible things in Selenium, but that's source material for another blog post later. In the mean time, stay salty!
I should clarify that we're working in the .Net world, specifically with C#. We're using Selenium WebDriver with Chrome (headless and not). If some other version of Selenium fixes this or if there's another tool that does what Selenium does without these issues, then, great - I'd love to hear about it! If it means switching our front end to some specific framework or abandoning certain kinds of tests, then now is the time for you to sit quietly in the back and Tweet about how much you love the sound of your own voice.
The first of the design decisions that make me want to bring the beat-down on my keyboard is how Selenium handles page objects that aren't visible. Let's consider the test case. Let's say I'm working on an application where data is sensitive and certain user's shouldn't be able to see some data. In other words, just about every website worth having tests for. I log in as user A, create some sensitive data, log out, log in as user B, try to view that data, then I want to show that the data is not visible. Easy, right? So, how has Selenium screwed this up? If I ask Selenium to give me that object that should no longer be visible, it doesn't give me the chance to ask if it's displayed or not - Selenium just throws an exception and blows up my whole test.
Some of you might be thinking that Selenium does support this - you just have to ask for all the instances of your element and check that the count is zero. Yes, you can do that, but it's a crap solution. First of all, that means you need to ask for one object two different ways based on what you expect to do with it. If you have helper methods that encapsulate the knowledge of how to get your application objects, now you have to double the number of helper methods to handle the cases.
Assert.IsTrue(CogApplication.GetAdministratorLink().Displayed,
"The admin should be able to see this.");
// Log out, log in as other user
Assert.AreEqual(0, CogApplication.GetAdministratorLinkWhenIDoNotExpectItToComeBackBecauseSeleniumMakesMeWriteCrapMethods(),
"I need a vacation now. Maybe Norway. They have lovely lakes.");
Secondly, even if you don't mind writing twice as many helpers as should have to, you still end up with brittle tests. Let's say that you have more than one developer working on your application (extraordinary situation, I know). On the page about sprockets, Developer Bobby decided that the setup link should not even be an element on the page when it's not there. You test it and it works fine. Now you're on the page about gears and Developer Jan decided that the setup link is on the page, but hidden. Now Selenium returns the page object, but the Displayed property is false. Your test is unhappy, you're unhappy, and Jan doesn't understand why you replaced all letters on their keyboard with spikes.
So, what have we done to design around Selenium's foolhardy decision? We had to start at the web driver.
public IWebDriver GetDriver()
{
return new SafeDriver(GetChromeDriver());
}
The SafeDriver class is a thin wrapper on the Chrome driver (or the PhantomJS driver, back before PhantomJS was wrapped in moth balls and dumped in the sea). It basically calls the core driver for anything it's asked for, except when you ask it for an element.
public IWebElement FindElement(By by)
{
return new SafeElement(this, by);
}
So, SafeDriver's only job is to return SafeElements every time we ask for an element. The real magic is in SafeElement.
public class SafeElement : IWebElement
{
public SafeElement(IWebDriver driver, By by)
{
_driver = driver; // store for later
_by = by;
}
public bool Dispalyed
{
get
{
try
{
return _driver.FindElement(_by).Displayed;
}
catch (NoSuchElementException)
{
return false;
}
}
}
// All other properties call _driver.FindElement(_by)
// and then the method
}
This isn't a complete solution. This only helps out with the 'displayed' issue. However, I'll say that storing off the driver and the 'by' let us do some nifty things later.
Also, this doesn't fix the issue where, somewhere in your chain, you call FindElements. Because FindElments returns a list and lists have a very large interface, I haven't taken the time to try and work around that and use delayed evaluation of the 'by' condition. Writing more specific FindElement conditions has worked out pretty well for us so far (passing indexes into XPath statements, etc).
Wrapping your driver in a SafeDriver isn't a perfect solution; but, at least you don't have to worry about 'in what way' an element is not displayed. It's better than stabbing your monitor. Or shouting at the developers. Well, I can't promise you won't still do those things. There are other terrible things in Selenium, but that's source material for another blog post later. In the mean time, stay salty!
Comments
Post a Comment