Check out "Do you speak JavaScript?" - my latest video course on advanced JavaScript.
Language APIs, Popular Concepts, Design Patterns, Advanced Techniques In the Browser

Testing with headless browser (Zombie.js + Jasmine)

I was planning to post this article a few weeks ago, but today I finally finished the examples and I can't wait to share it. We all know that the testing is a big part of our daily tasks and we should use every existing tool to make our lives easier. I really like test driven development and once I found the concept of the headless browsing I started to think that we can automate every single aspect of the testing. In this post I'll show you how to use Nodejs to test a simple php site.

All the source files in this post could be found here https://github.com/krasimir/blog-posts/tree/master/2012/TestingWithZombieJS. First of all, let's see how our test site looks like (feel free to browse it here. Username: admin, password: 1234). The site's content is protected with a password. So initially a login form is shown: [1] (If the user type wrong credentials the login form is shown again). The home page presents some text and two buttons. Logout removes the current session of the user and CSS selectors points to another page with text. [2] The second page of the site: [3] It is really simple web site, but it is enough to illustrate the idea. The topic of the article is to show the process of testing, so I'm not going to spend time in explaining how the site works. If you are interested in this part please check the source code. Here is what we will probably want to test in a real environment:

  • Can we actually access the site
  • We are not logged in the beginning, so we should see the login form
  • We should not be able to login via wrong credentials
  • We should be able to login with the correct credentials
  • We should be able to see the second page CSS selectors
  • We should be able to logout
  • We should not be able to see CSS selectors page because we are logged out

Without the usage of the headless browser we will need to test all those things manually. For such a simple application it will take no more then minute or two, but imagine if you have really solid web based system with bunch of features. It will take some time for sure. I'm going to use Zombie.js together with Jasmine under Nodejs. To install Nodejs on your system please check https://github.com/creationix/nvm. The NVM is really nice too, especially if you have to deal with different versions. Once you have Nodejs installed you should prepare your testing and install the necessary modules. The usage of the Node Package Manager (NPM) is recommended. So, create a directly where you will place your test, go there and create a package.json file with the following content:

    {
      "name": "TestingWithZombieJS",
      "version": "0.0.0",
      "description": "Testing using headless browser",
      "main": "index.js",
      "scripts": {
        "test": "echo \\"Error: no test specified\\" && exit 1"
      },
      "repository": "",
      "keywords": [
        "testing",
        "headless",
        "browser"
      ],
      "dependencies": {
        "zombie": "latest",
        "jasmine-node": "latest"
      },
      "author": "Krasimir Tsonev",
      "license": "BSD"
    }
    

(You can use npm init. It will help you to create the file.) Once you have package.json in your directory you could run npm install there and at the end you should have a folder called node_modules with zombiejs and jasmine-node modules inside. If you successfully finished the above steps, then you could start writing your test. Create an empty .js file (for example zombie.spec.js) and put the following code there:

    var Browser = require("zombie");
    var url = "https://github.com/krasimir/blog-posts/tree/master/2012/TestingWithZombieJS/site";
    var browser = new Browser();
    
    describe("testing with zombie", function() {
    
    });
    

When you run the test via

    jasmine-node zombie.spec.js
    

You should see the following result:

    Finished in 0.004 seconds
    0 tests, 0 assertions, 0 failures
    

The first thing that we should test - is our browser created and is our site actually accessible:

    var Browser = require("zombie");
    var url = "https://github.com/krasimir/blog-posts/tree/master/2012/TestingWithZombieJS/site";
    var browser = new Browser();
    
    describe("testing with zombie", function() {
    
        it("should have defined headless browser", function(next){
            expect(typeof browser != "undefined").toBe(true);
            expect(browser instanceof Browser).toBe(true);
            next();
        });
    
        it("should visit the site and see the login form", function(next) {
            browser.visit(url, function(err) {
                expect(browser.success).toBe(true);
                expect(browser.query("input[value='Login']")).toBeDefined();
                next();
            })
        });
    
    });
    

The result should be:

    Finished in 0.208 seconds
    2 tests, 4 assertions, 0 failures
    

Zombiejs's browser has bunch of helpful features, which you can use to check what is going on. For example in the code above we used .query, which accepts a valid CSS selector and returns a DOM element. We used this information to find out if the Login button is on the page, which means that the login form is shown. You could also use .html or .text to get the exact html markup, which is currently in the browser. I strongly recommend checking the Zombiejs API to find out what you can do. Here is the full test of site:

    var Browser = require("zombie");
    var url = "https://github.com/krasimir/blog-posts/tree/master/2012/TestingWithZombieJS/site";
    var browser = new Browser();
    
    describe("testing with zombie", function() {
    
        it("should have defined headless browser", function(next){
            expect(typeof browser != "undefined").toBe(true);
            expect(browser instanceof Browser).toBe(true);
            next();
        });
    
        it("should visit the site and see the login form", function(next) {
            browser.visit(url, function(err) {
                expect(browser.success).toBe(true);
                expect(browser.query("input[value='Login']")).toBeDefined();
                next();
            })
        });
    
        it("should not be able to login with wrong credentials", function(next) {
            browser
            .fill('input[name="username"]', "wrongname")
            .fill('input[name="password"]', "wrongpassword")
            .pressButton('input[value="Login"]', function() {
                expect(browser.html("body")).not.toContain("Insanely fast, headless full-stack testing using Node.js");
                expect(browser.query("input[value='Login']")).toBeDefined();
                next();
            });
        });
    
        it("should be able to login with correct credentials", function(next) {
            browser
            .fill('input[name="username"]', "admin")
            .fill('input[name="password"]', "1234")
            .pressButton('input[value="Login"]', function(res) {
                expect(browser.html("body")).toContain("Insanely fast, headless full-stack testing using Node.js");
                expect(browser.query("input[value='Login']")).toBeUndefined();
                next();
            });
        });
    
        it("should be able to see CSS selectors page", function(next) {
            browser.visit(url + "css-selectors", function(err) {
                expect(browser.html("body")).toContain("CSS Selectors");
                next();
            });
        });
    
        it("should logout", function(next) {
            browser.clickLink('#logout', function() {
                expect(browser.query("input[value='Login']")).toBeDefined();
                next();
            });
        });
    
        it("should not be able to see CSS selectors page after the logout", function(next) {
            browser.visit(url + "css-selectors", function(err) {
                expect(browser.html("body")).not.toContain("CSS Selectors");
                next();
            });
        });
    
    });
    

The final result:

    Finished in 1.274 seconds
    7 tests, 11 assertions, 0 failures
    

As you can see you have a fully working browser under your script control. You can fill forms, click buttons or links. You can also modify the request headers and all those things are programmable. By creating such an automated tests you could save a lot of time, which is normally spend in browsing the application. And, of course, the very best part, is that you will be notified where exactly is the problem.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn.