Webdriver with Node.js
Needed to automate some tests on a Node.js application that require usage of browsers. Have been doing this setup for a while so taking notes to avoid reinventing the wheel every time.
My overal idea is to start all needed services with docker-compose rather than requiring to install them.
I start one Webdriver Hub container, and one container per browser.
In tests I then only point Webdriver JS library to the hub.
Browsers need to access the application, but I did not want to start them in the host network as they would fight for ports.
So instead since my app runs fine in Docker I actually start it as another docker-compose service for it and will have to refer to it under http://app
hostname (port exposed for me to easily troubleshoot the application).
version: "3.4"
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- $PORT:$PORT
hub:
image: selenium/hub:$SELENIUM_VERSION
ports:
- 4444:4444
chrome:
image: selenium/node-chrome:$SELENIUM_VERSION
environment:
- SE_EVENT_BUS_HOST=hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
edge:
image: selenium/node-edge:$SELENIUM_VERSION
environment:
- SE_EVENT_BUS_HOST=hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
firefox:
image: selenium/node-firefox:$SELENIUM_VERSION
environment:
- SE_EVENT_BUS_HOST=hub
- SE_EVENT_BUS_PUBLISH_PORT=4442
- SE_EVENT_BUS_SUBSCRIBE_PORT=4443
The dockerfile is pretty straightforward and makes good use of layer caching to get fast builds:
FROM node:15.5.1-buster
EXPOSE 8081
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["npm", "start"]
I use a .env
file to keep track of configuration for both docker-compose (which reads it by default), the application and tests.
For Node.js I use dotenv to automatically load them.
Dotenv does not override values set by the command line so VAR=value npm start
still allow me to override for one particular command without touching the file, hence putting mostly defaults there.
PORT=8081
SELENIUM_VERSION=4.0.0
BROWSER_NAME=firefox
APP_BASE_URL=http://app:8081
HUB_URL=http://localhost:4444/wd/hub
My tests run with Mocha, and Webdriver being slow to start/stop I create an instance and reuses for all tests thanks to a root hook plugin. Configuration of Webdriver capabilities is done via environment variables to choose the browser. I use modules, and expose Webdriver instance as a mutable export (exports are references thus modules importing it will get the live value). Not very good for running tests in parallel, but fine for now.
import { Builder } from 'selenium-webdriver'
let capabilities = {
'browserName' : process.env.BROWSER_NAME,
'resolution' : '1024x768',
'headless': true
};
export const base_url = process.env.APP_BASE_URL;
export let driver;
export const mochaHooks = {
beforeAll : async function() {
driver = new Builder()
.usingServer(process.env.HUB_URL)
.withCapabilities(capabilities)
.build();
},
afterAll : async function() {
if (driver) {
await driver.quit();
}
}
};
Tests then import this module (which conveniently exposes base url too, to avoid computing it from environnment variable in every single test) and just execute.
import assert from 'assert'
import { driver, base_url } from '../webdriver.js'
describe('app', () => {
it('opens homepage', async () => {
await driver.get(`${base_url}/`);
let title = await driver.getTitle();
assert.equal('My app!', title);
});
});
The package.json
file keep tracks of dependencies and defines commands for running tests.
{
"scripts": {
"start": "node --require dotenv/config server.js",
"test": "mocha --require dotenv/config --require test/webdriver.js --recursive"
},
"type": "module",
"devDependencies": {
"mocha": "^8.3.2",
"selenium-webdriver": "^4.0.0-beta.3"
},
"dependencies": {
"dotenv": "^8.2.0"
}
}
With this organization, I can easily run tests on different browsers without installing any of them or dealing with driver binaries. I could also quite easily run against providers like Browserstack instead of using my local hub (would need to add authentication in capabilities).
npm test
BROWSER_NAME=firefox npm test
BROWSER_NAME=chrome npm test
BROWSER_NAME=MicrosoftEdge npm test