Testing Applications using Selenium and Mocha
Selenium seems to be a major player in the world of automated testing, right next to Nightwatch and PhantomJS. With the DevOps field exploding, it is becoming more and more clear that consistently testing an application is the best friend for a developer. Automatically running your code through a series of tools will not only make you feel like you know what you are doing, but it will also stomp out fire drills and save your team from dealing with bug fixes on the weekend by catching bugs or UI problems that humans might gloss over. Knowing very little about automated browser testing, I jumped head first into Selenium and integrated it into a recent project. Thinking it was going to be a hassle, as soon as I put together an easy way to write tests, everything became clear.
The beauty about this is that it can be used on any kind of web project, writing Selenium tests does not require any specific language, basically it just tells what the browser to do. Selenium tests an be written in Java, C#, Python, Ruby, PHP, Perl, or Javascript. I decided to run with JS since it is what I am most comfortable with, so turn around if you need examples in another language.
My directory structure looks like this:
project root
├ .babelrc
├ package.json
└ selenium-tests
├ index.js
└ cases
├ index.js
First things first, like always, you will need install some packages. Go to your project in a terminal and run:
npm install --save-dev selenium-webdriver chromedriver geckodriver mocha
Open up your package.json
file and under scripts, add a script that will run Mocha against your test file. I set --timeout 10000
which makes each test timeout after 10 seconds, I did that because the default is 5 seconds and sometimes Mocha would fail before the browser would boot up (of course that will depend on how fast the machine is that is running the tests, so this can be adjusted.) I like writing JS using the ES6 syntax, so I also set mocha to use the babel compiler and to pull in the babel-polyfill package. --bail
makes Mocha stop running after a test fails, this is good in case you have a lot of tests, if the first one fails now you do not have to wait for them all to finish failing.
{
...
"scripts": {
"selenium": "mocha --timeout 10000 --bail --compilers js:babel-core/register --require babel-polyfill ./selenium-tests/index.js"
}
...
}
Create the file selenium-tests/index.js
if you have not already and open it. You are going to need to import the Builder
module from the selenium-webdriver
package and also import the drivers for Chrome and Firefox. Then, import your test cases (we will get to that file later).
import { Builder } from 'selenium-webdriver'
import 'selenium-webdriver/firefox'
import 'selenium-webdriver/chrome'import cases from './cases/index'
Now have mocha spit out which browsers we will be testing in.
// ... imports up heredescribe('firefox', () => {
})describe('chrome', () => {
})
Before running each test I like to have mocha wait some time because the application might have animations, delays, AJAX requests; which can cause Selenium to freak out. Of course this will not work in every case, because AJAX requests can take much longer than 500ms, but this helps you not have to check the visibility of elements all the time, instead you can now check if an element exists. You can also set the timeout to whatever you like, but I feel like half a second is enough for most animations.
/// ... imports up heredescribe('firefox', () => {
beforeEach(done => {
setTimeout(() => {
done()
}, 500)
})
})describe('chrome', () => {
beforeEach(done => {
setTimeout(() => {
done()
}, 500)
})
})
We can now set up the driver for each browser. FYI, these will not work without the geckodriver
(for Firefox) or the chromedriver
(for Chrome, duh.) Generally people assign the build instance a variable name of driver
, I am using build
, but name it whatever you damn well please.
/// ... imports up heredescribe('firefox', () => {
beforeEach(done => {
setTimeout(() => {
done()
}, 500)
}) const build = new Builder()
.forBrowser('firefox')
.build()
})describe('chrome', () => {
beforeEach(done => {
setTimeout(() => {
done()
}, 500)
})
const build = new Builder()
.forBrowser('chrome')
.build()
})
Want the browsers to close automatically when all the tests are done? Easy:
/// ... imports up heredescribe('firefox', () => {
beforeEach(done => {
setTimeout(() => {
done()
}, 500)
}) const build = new Builder()
.forBrowser('firefox')
.build() after(() => {
return build.quit()
})
})describe('chrome', () => {
beforeEach(done => {
setTimeout(() => {
done()
}, 500)
})
const build = new Builder()
.forBrowser('chrome')
.build() after(() => {
return build.quit()
})
})
When I first started working on this I was writing out every single test case, which sucked. Instead, I figured out a “nicer” way by looping over an array of arrays then passing certain properties to a function with a specific argument structure. That way the list of tests do not look too jumbled, they take way less time to write, and can be re-used for every browser.
/// ... imports up heredescribe('firefox', () => {
beforeEach(done => {
setTimeout(() => {
done()
}, 500)
}) const build = new Builder()
.forBrowser('firefox')
.build() for (let i = 0; i < cases.length; i += 1) {
let [ func, ...args ] = cases[i]
func(build, ...args)
} after(() => {
return build.quit()
})
})describe('chrome', () => {
beforeEach(done => {
setTimeout(() => {
done()
}, 500)
})
const build = new Builder()
.forBrowser('chrome')
.build() for (let i = 0; i < cases.length; i += 1) {
let [ func, ...args ] = cases[i]
func(build, ...args)
} after(() => {
return build.quit()
})
})
Time to write some helper methods so we can have consistent test cases. Navigating to the first page of your app is the first thing you want Selenium to do, so we will start there. Make a file under cases
named _navigates.js
and add this to it:
export default function navigates (build) {
it('navigates', () => {
return build
.navigate()
.to('http://localhost:3000/')
})
}
A helper that tells us if an element is located or not would be useful. Make a new file under cases
named _elementLocated.js
and add this to it:
import { until } from 'selenium-webdriver'export default function elementLocated (build, msg = 'Locates an element and runs a callback', by, callback = () => {}) {
it (msg, () => {
return build
.wait(until.elementLocated(by))
.then(callback)
})
}
Finally we can write a couple test cases. Open up the index.js
file in the cases
directory and set it up like this:
import { By } from 'selenium-webdriver'import navigates from './_navigates'
import elementLocated from './_elementLocated'const sendKeys = (keys) => {
return (args) => {
args.sendKeys(keys)
}
}export default [
[ navigate ],
[ elementLocated, 'Finds the login form', By.css('form') ],
[ elementLocated, 'Finds and fills in the email form', By.name('session[email]'), sendKeys('email@email.com') ]
]
Go to your project’s root directory in your terminal and run npm run selenium
and watch it go! Selenium has way too many capabilities to cover here, so now that you have the test suite set up you should go check out the Selenium JS docs to see what else you can implement. ⌨️ 🖱