Frontend headless browser testing with CasperJS

A big cost area in testing is manual tests of the user interface. Therefore, a wide range of tools have been developed to automate running tests at the HTTP level. Selenium is a popular tool implemented in Java, for example. In the Node.js world, we have a few interesting choices. The chai-http plugin to Chai would let us interact at the HTTP level with the Notes application, while staying within the now-familiar Chai environment. ZombieJS is a popular tool that implements a simulated browser which we can direct to visit web pages and perform simulated clicks on simulated buttons, verifying application behavior by inspecting the resulting behavior.

However, for this section, we'll use CasperJS (http://casperjs.org). This tool runs on top of PhantomJS, which is a headless version of Webkit. Webkit is a real browser engine that's at the heart of the Safari browser. PhantomJS encapsulates Webkit so it runs completely headless, meaning that there is no GUI window popping up on the screen during test execution. It otherwise behaves exactly as a web browser, and CasperJS can even generate screenshots if its desired to verify visual elements.

While CasperJS is installed using npm, and CasperJS scripts are written in JavaScript that looks like Node.js, the CasperJS website repeatedly says that this is not Node.js. ES-2015 constructs do not work, and while there's a require statement it doesn't access Node.js packages. Fortunately, it comes with a fairly rich API that supports a wide range of tasks including screen scraping and UI test automation.

CasperJS scripts are run using the casperjs command. When run as casperjs test, additional methods are made available that are useful for functional testing.

Setup

Let's first set up the directory and other configurations:

$ mkdir test-compose/notesui

Then in test-compose/docker-compose.yml, update the volumes as follows:

notesapp-test:
 ..
 volumes:
 - ./reports-notes:/reports
 - ./notesui:/usr/src/app/notesui-test
 - ./notesmodel:/usr/src/app/notesmodel-test

This ensures that the directory just created appears as notesui-test in the application directory.

In run.sh, add this line to install CasperJS and PhantomJS:

docker exec -it notesapp-test npm install -g [email protected] [email protected]

In the script we're about to write, we need a user account that we can use to log in and perform some actions. Fortunately, we already have a script to set up a test account. In users/package.json, add this line to the scripts section:

"setupuser": "PORT=3333 node users-add",

Then in notes/package.json, add this line to the scripts section:

"test-docker-ui": "cd notesui-test && casperjs test uitest.js"

We're about to write this test script, but let's finish the setup, the final bit of which is adding these lines to run.sh:

docker exec -it userauth-test npm run setupuser
docker exec -it notesapp-test npm run test-docker-ui

When executed, these two lines ensure that the test user is set up, and it then runs the user interface tests.

Improving testability in Notes UI

Adding a few id or class attributes to HTML elements can improve testability. In the test we're about to write, we'll inspect the HTML to ensure that the browser has gone to the right page, and that the application is in the expected state. We'll also be clicking on buttons. Both of these tasks are made easier if we can refer to certain HTML elements by an id.

In notes/views/pageHeader.ejs, change these lines:

<% if (user && typeof hideAddNote === 'undefined') { %>
<a class="btn btn-primary" id="btnaddnote" href='/notes/add'>
  ADD Note</a>
<% } %>

<% if (user) { %>
<a class="btn btn-primary" id="btnlogout" href="/users/logout">
  Log Out <span class="badge"><%= user.username %></span></a>
<% } else { %>
<a class="btn btn-primary" id="btnloginlocal" href="/users/login">Log in</a>
<a class="btn btn-primary" id="btnlogintwitter" href="/users/auth/twitter"><img width="15px" src="/images/twitter-brand-logos/TwitterLogo_white.png"/>Log in with Twitter</a>
<% } %>

In notes/views/noteview.ejs, make these changes:

<a class="btn btn-default" id="btndestroynote" href="/notes/destroy?key=<%= notekey %>" role="button">Delete</a>
<a class="btn btn-default" id="btneditnote" href="/notes/edit?key=<%= notekey %>" role="button">Edit</a>

In both cases, we added the id attributes. In writing the test code, the id attribute made it easier to check for or click on these buttons.

CasperJS test script for Notes

In test-compose/notesui, create a file named uitest.js containing the following:

var notes = 'http://localhost:3000';
casper.test.begin('Can login to Notes application',
                  function suite(test) {
    casper.start(notes, function() {
        test.assertTitle("Notes");
        test.assertExists('a#btnloginlocal',
                          "Login button is found");
        this.click("a#btnloginlocal");
    });
..
    casper.run(function() {
        test.done();
    });
});

The casper.test.begin function contains a block of tests. You see that the callback is given a test object that has methods useful for testing. Inside this block, certain casper methods are called, the result of which is to create a queue of test instructions to perform. That queue is executed when casper.run is called.

The casper.start function must be called, well, at the start of the script. You give it a URL, as shown here, and you can optionally pass in a callback function.

The casper.run function must be called at the end of the script. In between these two, you'll write steps of the test scenario.

In callback functions, you can make the test assertions or you can call other casper functions that are available on this. In this case, the browser is on the Notes home page and we're checking a couple things to ensure that this is where the browser is located and that the browser is not logged in.

For example, because of the change we made to notes/views/pageHeader.ejs, the presence of #btnloginlocal is a sure indication that Notes is not logged in. If it were logged in, that button would not be visible and #btnlogout would be present instead.

When you take an action that causes an asynchronous request and response, it's necessary to start a new navigation step, that is the following:

casper.then(function() {
    test.assertHttpStatus(200);
    test.assertUrlMatch(/users/login/,
                        'should be on /users/login');
    this.fill('form', {
        username: "me",
        password: "w0rd"
    });
    this.click('button[type="submit"]');
});

We have just told the browser to click on the Login button. We should then end up on the /users/login page. We then put login parameters into the form inputs, and click on the Submit button.

This step is why we needed the setupuser script to be run:

casper.waitForSelector('#btnlogout', function() {
    test.assertHttpStatus(200);
   test.assertTitle("Notes");
    test.assertExists('a#btnlogout', "logout button is found");
    test.assertExists('a#btnaddnote', "Add Note button is found");
    this.click("#btnaddnote");
});

Once we click on the login form, we need to wait for the screen to refresh. It should, of course, navigate to the Notes home page. The waitForSelector method waits until an element with that selector is available on the page.

In CasperJS, many functions take selector parameters. These are used to select elements in the DOM of the current page. By default, it takes CSS3 selector strings, but you can also use XPath selectors.

The final step is to click on the Add Note button.

casper.waitForUrl(/notes/add/, function() {
 test.assertHttpStatus(200);
 test.assertTitle("Add a Note");
 test.assertField("docreate", "create");
 this.fill('form', {
   notekey: 'testkey',
   title: 'Test Note Title',
   body: 'Test Note Body with various textual delights'
 });
 this.click('button[type="submit"]');
});

And, of course, the response to that is to go to /notes/add. We again check a few things to make sure that the browser has gone to the correct page. We then fill in the entry form with a dummy note and click on the Submit button:

casper.waitForUrl(/notes/view/, function() {
 test.assertHttpStatus(200);
 test.assertTitle("Test Note Title");
 test.assertSelectorHasText("p#notebody",
    'Test Note Body with various textual delights');
 this.click('#btndestroynote');
});

The browser should of course go to /notes/view. We're checking a few parameters on the screen to verify this. We then click on the Destroy button:

casper.waitForUrl(/notes/destroy/, function() {
 test.assertHttpStatus(200);
 test.assertTitle("Test Note Title");
 test.assertField("notekey", "testkey");
 this.click('input[type="submit"]');
});

Once the browser gets to /notes/destroy, we check that the page indeed has all the right elements. We can click on the Submit button to verify that the note should be deleted:

casper.waitForUrl(notes, function() {
 test.assertHttpStatus(200);
 test.assertTitle("Notes");
 test.assertExists('a#btnlogout', "logout button is found");
 this.click("#btnlogout");
});

The browser should again be on the home page. Let's now verify the ability to log out by clicking on the Logout button:

casper.waitForUrl(notes, function() {
 test.assertHttpStatus(200);
 test.assertTitle("Notes");
 test.assertExists('a#btnloginlocal', "Login button is found");
});

The browser should again be on the home page, but logged out. We distinguish between being logged-in and logged-out by which buttons are present.

Running the UI test with CasperJS

Now that you have the test entered, we can run it. Looking at run.sh, these steps will run just the UI test:

docker-compose stop
docker-compose up --build --force-recreate -d
docker exec -it notesapp-test npm install -g [email protected] [email protected]
docker exec -it userauth-test npm run setupuser
docker exec -it notesapp-test npm run test-docker-ui

The last step of which will look like this:

$ docker exec -it notesapp-test npm run test-docker-ui
npm info it worked if it ends with ok
npm info using [email protected]
npm info using [email protected]
npm info lifecycle [email protected]~pretest-docker-ui: [email protected]
npm info lifecycle [email protected]~test-docker-ui: [email protected]

> [email protected] test-docker-ui /usr/src/app
> cd notesui-test && casperjs test uitest.js

Test file: uitest.js                    
# Can login to Notes application
PASS Can login to Notes application (5 tests)
PASS Page title is: "Notes"
PASS Login button is found
PASS HTTP status code is: 200
PASS should be on /users/login
PASS HTTP status code is: 200
PASS Page title is: "Notes"
PASS logout button is found
PASS Add Note button is found
PASS HTTP status code is: 200
PASS Page title is: "Add a Note"
PASS "docreate" input field has the value "create"
PASS HTTP status code is: 200
PASS Page title is: "Test Note Title"
PASS Find "Test Note Body with various textual delights" within the selector "p#notebody"
PASS HTTP status code is: 200
PASS Page title is: "Test Note Title"
PASS "notekey" input field has the value "testkey"
PASS HTTP status code is: 200
PASS Page title is: "Notes"
PASS logout button is found
PASS HTTP status code is: 200
PASS Page title is: "Notes"
PASS Login button is found
PASS 23 tests executed in 88.227s, 23 passed, 0 failed, 0 dubious, 0 skipped.

While this report output looks cool, you probably want to generate a test results data file so your continuous build system (Jenkins, et al.) can display a green or red dot on the dashboard as appropriate. CasperJS supports outputting XUnit results as follows:

"test-docker-ui": "cd notesui-test && casperjs test uitest.js --xunit=/reports/notesui.xml"

Earlier we configured Mocha to output JSON that theoretically we can use to generate a test results report. In this case XUnit test results can be used the same way.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset