Headless browser testing: a play around with Taiko
Unit test frameworks for libraries like React are incredibly powerful and allow developers to write tests that test both rendering and actions within an application. However, this testing does not capture all issues that browser-based testing captures, typically via a Selenium-based toolset. Although incredibly useful, in fact critical, browser-based testing often involves slow feedback loops because it is normally run at a later stage in the CI pipeline and not readily available to execute whilst writing code. What if we could push parts of this browser-based testing closer to the point where we write code and into early phases of the CI pipeline for our feature branches using a lightweight execution environment that provides a quick feedback loop so we know when we’ve fundamentally broken something? What if we can validate code changes for a feature of bugfix in seconds or minutes? 🤔
Taiko
There are several tools in the space of headless browser testing, but a few weeks back I spotted a blog about the use of Taiko, which is an open source browser automation toolkit developed by the people at ThoughtWorks. It leverages Chrome and most importantly its development tools to perform the testing. OK, so we’re limited to Chrome, but the key thing is to identify key breakages early - things that are fundamentally broken! For example, does a button trigger the desired action or is an element rendered?
The great thing in my opinion about Taiko is that the tests are written in JavaScript and can be integrated with existing unit test frameworks such as Mocha. This means that you can leverage all of the powerful constructs available within a fully-fledged programming language. The following code snippet demonstrates the simplicity of interacting with a browser in a test (performs a google search)…
const { openBrowser, goto, write, click, closeBrowser } = require('taiko');
(async () => {
try {
await openBrowser({args: [...]});
await goto("google.com");
await write("Apache Kafka is awesome");
await click("Google Search");
} catch (error) {
console.error(error);
} finally {
await closeBrowser();
}
})();
Taking Taiko for a spin
This is surprisingly easy if you follow the instructions, but I wanted to take this a little further and create a Docker image that could be re-used to run various test scripts. So here’s what I did:
1 - Create a Dockerfile
This is where I created a generic Docker image with relevant dependencies installed, with the aim of having something reusable and decoupled from the actual test case(s) being executed - we don’t want to rebuild a Docker image each time we get a new test case. This image uses the node:10-slim Docker image as base (Debian), because it avoids the need to install a load of missing dependencies which are not on the Alpine image required for the Chrome dev tools.
However, as part of this I did create a non-root user to avoid needing to run the container as root, as per best practices. I omitted other defanging, but this can be layer on with relatively little effort. At runtime, the container ingests the test script and target URL via ENV vars. More on that later.
You end up with a Dockerfile that looks something like this…
FROM node:10-slim
USER root
WORKDIR /opt/demo/browser-test/
# Install a bunch of required dependencies for Chrome devtools...
RUN apt-get update && \
apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget \
--no-install-recommends
COPY ./scripts/run.sh .
RUN npm install -g taiko --unsafe-perm=true && \
# Add a non-privileged user...
groupadd -r demo && useradd -r -m -g demo demo && \
mkdir -p /home/demo/Downloads /app && \
chown -R demo:demo /home/demo && \
chown -R demo:demo /app && \
chown -R demo:demo /opt/demo/browser-test/ && \
chmod 744 ./run.sh
USER demo
CMD [ "/bin/bash", "run.sh" ]
Note: The run.sh script referenced above copies the test script from the ENV var and pushes into a file, then starts taiko.
set -e
echo $TEST_SCRIPT | base64 --d >> test.js && \
taiko test.js
2 - Build the image
docker build . -t test-runner
…it’s that simple
3 - Create a test script
Create a test.js file and paste the content below into it. This needs to live within the same folder as the Dockerfile for the purposes of this demonstration.
const { openBrowser, goto, write, click, closeBrowser } = require('taiko');
const URL = process.env.URL;
(async () => {
try {
await openBrowser({args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-first-run',
'--no-sandbox',
'--no-zygote']}
);
await goto(URL);
await write("Apache Kafka is awesome");
await click("Google Search");
} catch (error) {
console.error(error);
} finally {
await closeBrowser();
}
})();
4 - Running a Headless Browser Test
Run the following command, which takes the test.js file and pokes into the container as a Base64 encoded string. This works for small files and the example, but I’d imagine fetching content from a remote location would make more sense if there were more assets. E.g., the test scripts/resources could live within S3 and be pulled down at runtime based on some ENV vars or they could be mounted to the container via a K8S ConfigMap. There’s a series of suitable options.
docker run -it --rm \
-e URL="google.com" \
-e TEST_SCRIPT=$(cat test.js | base64) \
test-runner
…which yields the following output:
✔ Browser opened
✔ Navigated to URL http://google.com
✔ Wrote Apache Kafka is awesome into the focused element.
✔ Clicked element matching text “Google Search”
✔ Browser closed
Key Thoughts
Taiko in conjunction with Docker demonstrates the power of being able install and run with little effort, using a set of tools that are familiar with engineers - both dev and test. Whether it’s Taiko or another framework, I see headless browser testing becoming a fundamental part of CI pipelines, allowing for quicker feedback loops and empowering engineers to run these tests locally with little effort to gain confidence about the changes they are making. This should be seen as supplemental to the full browser testing we perform, maybe allowing us to reduce the burden on these tests to find issues and assure quality.