The test automation domain, at a high level, is comparatively simple. We run a linear set of instructions, we have a discrete expected final state, and tests either pass or fail. We generally don’t have broad scaling or concurrency issues, we know all the moving parts ahead of time, and we understand how those parts are supposed to interact. If the harness fails horribly, we can fix it and the end consumer never has to know.
Compare to a public-facing application where can consumers can misbehave, load can spike unexpectedly, malware can attack, and problems affect real customers.
Given this relatively simple domain, we ought to be able to take smart people and train them. If someone can grok Java they can certainly write Ruby. You might have to mentor them for a bit to not overcomplicate your code, but new contributors to your team should be coached for style regardless.
Do you believe in the abilities of your tech team ? Do you believe in your organization’s ability to train and develop talent ? If yes, then finding developers shouldn’t be a problem – just hire smart junior developers and train them well. If not, it’s time for some organizational soul-searching.
If you’re startup and don’t have the time – you hopefully have a senior person already on your founding team. Take a couple months to train some smart junior people, and you’ve tripled or quadrupled your dev group for the cost of one or two seniors. And you’ve developed a training culture and infrastructure so you can hire and train more juniors much more easily. You’ve traded a small group of senior devs for a) the staff you needed, b) a learning, improvement-based culture, and c) a much easier path to more staff in the future.
Hopefully then, that sorts the hiring argument ? Let’s get back to talking about the best tool for the job.
In automation, simplicity is the Most Important Thing
We want to spend less time dealing with bugs in our test code, and more time finding bugs in our actual product. Thus, we want the most reliable test harness possible.
Our problem is not so much at the beginning of an automation effort, but rather the middle. Not the first round of problems, but the second and third round and beyond, when complexity compounds with more complexity, developers work around existing work arounds, and a culture is established of sloppy code and reactionary fire-fighting.
In manufacturing, a key metric of reliability is “number of moving parts”. Less moving parts means less friction, less wear, less materials fatigue and failure. Indeed, the “solid state” revolution of the 50s and 60s, which basically enabled our entire modern technological world, was largely a shift from moving to non-moving parts, with exponential improvements in efficiency and reliability. So less moving parts – more simplicity – is better.
Unfortunately in the engineering mindset, simplicity is often a secondary consideration, or not at all. If it’s complicated, someone smart must have designed it, yes ? If it’s technical to explain, it must be high-tech, right?
In the immortal words of Colin Chapman, founder of Lotus Motorsport, “To go faster: simplify, then add lightness.”
Java adds complexity for zero benefit
We’re adding a lot of complexity using Java, in two distinct ways:
Comparatively more words for a given operation. “public final static int”, or declaring -> defining -> storing a HashMap, are just a couple examples. Say what you like about static typing; I would say, for automation, it’s pointless. You’ve increased syntax by an order of magnitude and you’re still not going to get away from the NPE’s in your crappy Java code.
The Java mindset tends to think in too many layers:
- Define an abstract class,
- Implement the abstract class,
- Declare multiple interfaces for related actions and implement those,
- Okay, now we can actually do something.
- Let’s throw in a private inner class because we can only have one declared class per file.
- Oh yeah, let’s worry about package security as well.
Given all this, we see that especially in the Java world, complexity breeds complexity. Bugs cascade upon bugs, and drilling down to the actual cause of an issue becomes way too complicated.
What a great way to justify more delays, more person-hours for fixing a project ! We can write tons of code and not accomplish much of anything at all !
Since JS comes from the browser world, we spend a lot of time managing wait states with promises, async / await declarations, or whatever the next async construct will be.
JS acolytes – and I’m definitely a JS fan – would say all this sophisticated async structuring makes for fast operation. But consider our automation domain: just how important is the execution speed of a code block ? We are device, browser, API or IO restricted – rarely CPU bound. If we’re talking millions of users on a server, fine. But automation is not such a domain.
Again, simplicity is key. Declaring and managing async state, passing around callback functions, is more work, more items to manage, more moving parts that can break.
Let’s be honest about our coding culture – we can have lots of syntax and code to point at, lots of intricate callbacks and listeners and interceptors. But if we’re trying perform just a straightforward operation, what value have we actually added ? Or are we just reveling in details, like a dog rolling in … ? Let’s stay disciplined ! Now in the right context, I’m all for intricate design and coding adventures. Let’s try new things and experiment, absolutely ! But your critical automation infrastructure is not the place. Intricate designs for creativity’s sake, poking around the bleeding edge – let’s leave that to hackathons, betas and side projects, at least until the ideas mature.
Now let’s talk about how, for automation at least, Ruby beats JS to bits.
Ruby Automation Advantage #1: Reflection and breakpointing
A major challenge in automation when first developing a test, and then later when trying to isolate a test failure, is the length of time it takes for a functional test to advance long enough to encounter a particular problem. Your app may need to login, navigate several screens, and set up state along the way, before your specific test is ready to execute. If your bug or task takes several iterations to try a solution, fail, then setup again for another attempt, you’re looking at several minutes for each iteration.
What a way to waste a day !
Here’s an alternative: set a breakpoint and poke around !
Now clearly, you can breakpoint and interact in Java, JS and other languages. But Ruby’s capacity here is, to steal a phrase, “Best of Breed”:
Now what if I told you, not only can you poke around, but when you find a solution – you can change your code, save, reload while staying within the breakpoint context, and continue without having to reinitialize ?
Advantage #2: Hot Reloading is the New Hotness
No more waiting 5, 10 times or more for a lengthy test setup, seeing a failure, adjusting a fix, trying again ad infinitum. Just hot-reload, try the fix, hot-reload again. Seconds instead of minutes or hours. Repeat this over a week or month’s automation work and the potential for Ferengi business expansion is staggering.
Advantage #3: No-parens and the joy they bring
Unlike most languages, in Ruby parens() are optional for function calls. These lines are equivalent:
browser.window.resize_to(800, 600) browser.window.resize_to 800, 600
It’s a small detail at first blush, but the repercussions are significant. For example, consider this block of Ruby code, which is a definition of a page object model using the excellent Site Prism framework:
class LoginPage < SitePrism::Page set_url '/login.html' element :un, 'input#username' element :pw, 'input#password' element :submit, 'input#submit' end
A Cucumber test execution might look like this:
Given("I login with valid credentials") do $LoginPage.load $LoginPage.un.set 'email@example.com' $LoginPage.pw.set ENV['TESTPW'] $LoginPage.submit.click $LoginPage.should_not have_error $Dashboard.should be_displayed end
The corresponding block in JS is full of curlybraces and parens. The Java implementation is worse.
Better readability means less time means money saved !
Advantage #4: DRYer is better
Ruby’s flexible syntax encourages clean code and “Don’t Repeat Yourself” practices. In JS and certainly in Java, the up-front work of creating a new function, configuration, or class, is enough trouble that it’s tempting to copy-paste. I know I am certainly guilty of it from time to time.
In contrast, Ruby is once again the “original and best” at making it easy to re-package and re-factor code with better syntax, namespacing, modular architecture, and more.
When your core programming syntax makes refactoring easy, you’re giving your development team the tools to write better code.
Wrapping up for now
There’s much more to talk about including Least Astonishment, Domain Specific Languages, a word about Python, dependency management with Bundler and Rubygems, and more. But this is plenty for a lead-off article.
Just remember Rule of Acquisition #22: “A wise man can hear profit in the wind.”