Advertisement
JavaScript & AJAX

Testing JavaScript with PhantomJS

by

I don't think I need to convince you that testing your JavaScript code is a good idea. But, it can sometimes prove tedious to test JavaScript code that requires a DOM. This means you need to test your code in the browser and can't use the terminal, right? Wrong, actually: enter PhantomJS.


What exactly is PhantomJS? Well, here's a blurb from the PhantomJS website:

PhantomJS is a headless WebKit with JavaScript API.

As you know, Webkit is the layout engine that Chrome, Safari, and a few other niche browsers use. So PhantomJS is a browser, but a headless browser. This means that the rendered web pages are never actually displayed. This may sound weird to you; so you can think of it as a programmable browser for the terminal. We'll look at a simple example in a minute, but we first need to install PhantomJS.


Installing PhantomJS

Installing PhantomJS is actually pretty simple: it's just a single binary that you download and stick in your terminal path. On the PhantomJS download page, choose your operating system and download the correct package. Then move the binary file from the downloaded package to a directory inside your terminal path (I like to put this kind of thing in ~/bin).

If you're on Mac OS X, there's a simpler way to install PhantomJS (and this is actually the method I used). Just use Homebrew, like this:

brew update && brew install phantomjs

You should now have PhantomJS installed. You can double-check your installation by running this:

phantomjs --version

I'm seeing 1.7.0; you?


A Small Example

Let's start with a small example.

simple.js
console.log("we can log stuff out.");

function add(a, b) {
    return a + b;
}

conslole.log("We can execute regular JS too:", add(1, 2));

phantom.exit();

Go ahead and and run this code by issuing the following command:

phantomjs simple.js

You should see the output from the two console.log lines in your terminal window.

Sure, this is simple, but it makes a good point: PhantomJS can execute JavaScript just like a browser. However, this example doesn't have any PhantomJS-specific code...well, apart from the last line. That's an important line for every PhantomJS script because it exits the script. This might not make sense here, but remember that JavaScript doesn't always execute linearly. For example, you might want to put the exit() call in a callback function.

Let's look at a more complex example.


Loading Pages

Using the PhantomJS API, we can actually load any URL and work with the page from two perspectives:

  • as JavaScript on the page.
  • as a user looking at the page.

Let's start by choosing to load a page. Create a new script file and add the following code:

script.js
var page = require('webpage').create();

page.open('http://net.tutsplus.com', function (s) {
    console.log(s);
    phantom.exit();
});

We start by loading PhantomJS' webpage module and creating a webpage object. We then call the open method, passing it a URL and a callback function; it's inside this callback function that we can interact with the actual page. In the above example, we just log the status of the request, provided by the callback function's parameter. If you run this script (with phantomjs script.js), you should get 'success' printed in the terminal.

But let's make this more interesting by loading a page and executing some JavaScript on it. We start with the above code, but we then make a call to page.evaluate:

page.open('http://net.tutsplus.com', function () {
    var title = page.evaluate(function () {
        var posts = document.getElementsByClassName("post");
        posts[0].style.backgroundColor = "#000000";
        return document.title;
    });
    page.clipRect = { top: 0, left: 0, width: 600, height: 700 };
    page.render(title + ".png");
    phantom.exit();
});

PhantomJS is a browser, but a headless browser.

The function that we pass to page.evaluate executes as JavaScript on the loaded web page. In this case, we find all elements with the post class; then, we set the background of the first post to black. Finally, we return the document.title. This is a nice feature, returning a value from our evaluate callback and assigning it to a variable (in this case, title).

Then, we set the clipRect on the page; these are the dimensions for the screenshot we take with the render method. As you can see, we set the top and left values to set the starting point, and we also set a width and height. Finally, we call page.render, passing it a name for the file (the title variable). Then, we end by calling phantom.exit().

Go ahead and run this script, and you should have an image that looks something like this:

You can see both sides of the PhantomJS coin here: we can execute JavaScript from inside the page, and also execute from the outside, on the page instance itself.

This has been fun, but not incredibly useful. Let's focus on using PhantomJS when testing our DOM-related JavaScript.


Testing with PhantomJS

Yeoman uses PhantomJS in its testing procedure, and it's virtually seamless.

For a lot of JavaScript code, you can test without needing a DOM, but there are times when your tests need to work with HTML elements. If you're like me and prefer to run tests on the command line, this is where PhantomJS comes into play.

Of course, PhantomJS is not a testing library, but many of the other popular testing libraries can run on top of PhantomJS. As you can see from the PhantomJS wiki page on headless testing, PhantomJS test runners are available for pretty much every testing library you might want to use. Let's look at how to use PhantomJS with Jasmine and Mocha.

First, Jasmine and a disclaimer: there isn't a good PhantomJS runner for Jasmine at this time. If you use Windows and Visual Studio, you should check out Chutzpah, and Rails developers should try guard-jasmine. But other than that, Jasmine+PhantomJS support is sparse.

For this reason, I recommend you use Mocha for DOM-related tests.

HOWEVER.

It's possible that you already have a project using Jasmine and want to use it with PhantomJS. One project, phantom-jasmine, takes a little bit of work to set up, but it should do the trick.

Let's begin with a set of JasmineJS tests. Download the code for this tutorial (link at the top), and check out the jasmine-starter folder. You'll see that we have a single tests.js file that creates a DOM element, sets a few properties, and appends it to the body. Then, we run a few Jasmine tests to ensure that process did indeed work correctly. Here's the contents of that file:

tests.js
describe("DOM Tests", function () {
    var el = document.createElement("div");
    el.id = "myDiv";
    el.innerHTML = "Hi there!";
    el.style.background = "#ccc";
    document.body.appendChild(el);

    var myEl = document.getElementById('myDiv');
    it("is in the DOM", function () {
        expect(myEl).not.toBeNull();
    });

    it("is a child of the body", function () {
        expect(myEl.parentElement).toBe(document.body);
    });

    it("has the right text", function () {
        expect(myEl.innerHTML).toEqual("Hi there!");
    });

    it("has the right background", function () {
        expect(myEl.style.background).toEqual("rgb(204, 204, 204)");
    });
});

The SpecRunner.html file is fairly stock; the only difference is that I moved the script tags into the body to ensure the DOM completely loads before our tests run. You can open the file in a browser and see that all the tests pass just fine.

Let's transition this project to PhantomJS. First, clone the phantom-jasmine project:

git clone git://github.com/jcarver989/phantom-jasmine.git

This project isn't as organized as it could be, but there are two important parts you need from it:

  • the PhantomJS runner (which makes Jasmine use a PhantomJS DOM).
  • the Jasmine console reporter (which gives the console output).

Both of these files reside in the lib folder; copy them into jasmine-starter/lib. We now need to open our SpecRunner.html file and adjust the <script /> elements. Here's what they should look like:

<script src="lib/jasmine-1.2.0/jasmine.js"></script>
<script src="lib/jasmine-1.2.0/jasmine-html.js"></script>
<script src="lib/console-runner.js"></script>
<script src="tests.js"></script>

<script>
    var console_reporter = new jasmine.ConsoleReporter()
    jasmine.getEnv().addReporter(new jasmine.HtmlReporter());
    jasmine.getEnv().addReporter(console_reporter);
    jasmine.getEnv().execute();
</script>

Notice that we have two reporters for our tests: an HTML reporter and a console reporter. This means SpecRunner.html and its tests can run in both the browser and the console. That's handy. Unfortunately, we do need to have that console_reporter variable because it's used inside the CoffeeScript file we're about to run.

So, how do we go about actually running these tests on the console? Assuming you're in the jasmine-starter folder on the terminal, here's the command:

phantomjs lib/run\_jasmine\_test.coffee ./SpecRunner.html

We're running the run\_jasmine\_test.coffee script with PhantomJS and passing our SpecRunner.html file as a parameter. You should see something like this:

Of course, if a test fails, you'll see something like the following:

If you plan on using this often, it might be a good idea to move run\_jasmine\_test.coffee to another location (like ~/bin/run\_jasmine\_test.coffee) and create a terminal alias for the whole command. Here's how you'd do that in a Bash shell:

alias phantom-jasmine='phantomjs /path/to/run\_jasmine\_test.coffee'

Just throw that in your .bashrc or .bash_profile file. Now, you can just run:

phantom-jasmine SpecRunner.html

Now your Jasmine tests work just fine on the terminal via PhantomJS. You can see the final code in the jasmine-total folder in the download.


PhantomJS and Mocha

Thankfully, it's much easier to integrate Mocha and PhantomJS with mocha-phantomjs. It's super-easy to install if you have NPM installed (which you should):

npm install -g mocha-phantomjs

This command installs a mocha-phantomjs binary that we'll use to run our tests.

In a previous tutorial, I showed you how to use Mocha in the terminal, but you'll do things differently when using it to test DOM code. As with Jasmine, we'll start with an HTML test reporter that can run in the browser. The beauty of this is we'll be able to run that same file on the terminal for console test results with PhantomJS; just like we could with Jasmine.

So, let's build a simple project. Create a project directory and move into it. We'll start with a package.json file:

{
    "name": "project",
    "version": "0.0.1",
    "devDependencies": {
        "mocha": "*",
        "chai" : "*"
    }
}

Mocha is the test framework, and we'll use Chai as our assertion library. We install these by running NPM.

We'll call our test file test/tests.js, and here are its tests:

describe("DOM Tests", function () {
    var el = document.createElement("div");
    el.id = "myDiv";
    el.innerHTML = "Hi there!";
    el.style.background = "#ccc";
    document.body.appendChild(el);

    var myEl = document.getElementById('myDiv');
    it("is in the DOM", function () {
        expect(myEl).to.not.equal(null);
    });

    it("is a child of the body", function () {
        expect(myEl.parentElement).to.equal(document.body);
    });

    it("has the right text", function () {
        expect(myEl.innerHTML).to.equal("Hi there!");
    });

    it("has the right background", function () {
        expect(myEl.style.background).to.equal("rgb(204, 204, 204)");
    });
});

They're very similar to the Jasmine tests, but the Chai assertion syntax is a bit different (so, don't just copy your Jasmine tests).

The last piece of the puzzle is the TestRunner.html file:

<html>
    <head>
        <title> Tests </title>
        <link rel="stylesheet" href="./node_modules/mocha/mocha.css" />
    </head>
    <body>
        <div id="mocha"></div>
        <script src="./node_modules/mocha/mocha.js"></script>
        <script src="./node_modules/chai/chai.js"></script>
        <script>
            mocha.ui('bdd'); 
            mocha.reporter('html');
            var expect = chai.expect;
        </script>
        <script src="test/test.js"></script>
        <script>
            if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
            else { mocha.run(); }
        </script>
    </body>
</html>

There are several important factors here. First, notice that this is complete enough to run in a browser; we have the CSS and JavaScript from the node modules that we installed. Then, notice the inline script tag. This determines if PhantomJS is loaded, and if so, runs the PhantomJS functionality. Otherwise, it sticks with raw Mocha functionality. You can try this out in the browser and see it work.

To run it in the console, simply run this:

mocha-phantomjs TestRunner.html

Voila! Now you're tests run in the console, and it's all thanks to PhantomJS.


PhantomJS and Yeoman

I'll bet you didn't know that the popular Yeoman uses PhantomJS in its testing procedure, and it's vritually seemless. Let's look at a quick example. I'll assume you have Yeoman all set up.

Create a new project directory, run yeoman init inside it, and answer 'No' to all the options. Open the test/index.html file, and you'll find a script tag near the bottom with a comment telling you to replace it with your own specs. Completely ignore that good advice and put this inside the it block:

var el = document.createElement("div");
expect(el.tagName).to.equal("DIV");

Now, run yeoman test, and you'll see that the test runs fine. Now, open test/index.html file in the browser. It works! Perfect!

Of course, there's a lot more you can do with Yeoman, so check out the documentation for more.


Conclusion

Use the libraries that extend PhantomJS to make your testing simpler.

If you're using PhantomJS on its own, there isn't any reason to learn about PhantomJS itself; you can just know it exists and use the libraries that extend PhantomJS to make your testing simpler.

I hope this tutorial has encouraged you to look into PhantomJS. I recommend starting with the example files and documentation that PhantomJS offers; they'll really open your eyes to what you can do with PhantomJS--everything from page automation to network sniffing.

So, can you think of a project that PhantomJS would improve? Let's hear about it in the comments!

Related Posts
  • Code
    Web Development
    Build Your Own Yeoman GeneratorYeoman wide retina preview
    Take a look at some of the more interesting features of Yeoman by learning how to build your own Yeoman generator.Read More…
  • Code
    JavaScript & AJAX
    JavaScript Tools of the Trade: JSBinJsbin retina preview
    We've all been there. There are times when you simply want to throw some JavaScript code up and see how it works. Sure, you could go through the hassle of:Read More…
  • Code
    JavaScript & AJAX
    Ember.js TestingEmber components retina preview
    When I started playing around with Ember.js almost a year ago, the testability story left something to be desired. You could unit test an object without any trouble, but a unit test is only one way to get feedback when you're building a software product. In addition to unit tests, I wanted a way to verify the integration of multiple components. So like most people testing rich JavaScript applications, I reached for the mother of all testing tools, Selenium.Read More…
  • Code
    JavaScript & AJAX
    Testing in Node.jsNodejs testing chai retina preview
    A test driven development cycle simplifies the thought process of writing code, makes it easier, and quicker in the long run. But just writing tests is not enough by itself, knowing the kinds of tests to write and how to structure code to conform to this pattern is what it's all about. In this article we will take a look at building a small app in Node.js following a TDD pattern.Read More…
  • Code
    PHP
    Acceptance Testing With CodeceptionIntro to codeception retina preview
    Typically new features for web applications are tested by visiting the appropriate page in a browser, maybe filling out some form data, submitting the form, and then developers or testers hope to see their desired result. This is the natural way most web developers test their apps. We can continue with this natural testing process and improve upon it to ensure our apps are as stable as possible by using Codeception. Read More…
  • Code
    Tools
    Your Obedient Assistant: YeomanCode
    One thing is certain: times sure have changed. Not too long ago, front-end development, though not simple, was manageable. Learn HTML, a bit of CSS, and you’re on your way. These days, however, for lack of better words, there are far more variables to juggle. Preprocessors, performance tuning, testing, image optimization, and minification represent but just a few of the key factors that the modern day front-end developer must keep in mind.Read More…