keyword

Keyword-driven testing library

npm install keyword
6 downloads in the last week
12 downloads in the last month

Keyword.js

A Keyword-driven testing library for node.

The library allows you to write low-level keywords that can be used for integration testing. By combining the low-level keywords, you can create new high-level keywords without any actual coding. Low-level keyword maps to a JavaScript function, where as high-level keyword contains only other high or low-level keywords.

Hello World example

Let's write the first low-level keywords Hello World, which prints "Hello World" and How are you? which prints "How are you?", obviously.

// lowlevel-keywords.js

var lowlevelKeywords = {
    "Hello World": function(next) {
        console.log("Hello World");
        next();
    },
    "How are you?": function(next) {
        console.log("How are you?");
        next();
    }
};

module.exports = lowlevelKeywords;

Pretty simple stuff.

Let's create our first high-level keyword Greet the World, which says hello and asks how is it going.

// highlevel-keywords.js

var highlevelKeywords = {
    "Greet the World": [
        "Hello World",
        "How are you?"
    ]
};

module.exports = highlevelKeywords;

The syntax is following:

  • Keywords are defined as a map (plain JavaScript object) of keyword name and a function (for low-level keywords) or an array of keywords to run (for high-level keywords)

Next, we want to run our keywords. Here's the code that runs the keyword Greet the World:

// basic.js

// Require keyword library
var key = require('keyword');

// Import keyword definitions
key(require('./lowlevel-keywords'));
key(require('./highlevel-keywords'));

key.run("Greet the World").then(function() {
    // All done.
});

Now we can run the example by typing

$ node basic.js

Output

> Hello World
> How are you?

Click here to see the whole example

Keywords with params and return values

In the basic example, all the keyword were static. They didn't take any parameter nor did they return anything.

Both, low-level and high-level keywords can take parameters and return values.

Let's define three low-level keyword:

  • Print takes message as a parameter and prints it to the console log.
  • Hello takes name and returns a string saying "Hello" and the name of the person.
  • Join takes two strings and joins them into one string, separated by a newline
// lowlevel-keywords.js

var lowlevelKeywords = {
    "Print": function(next, message) {
        console.log(message);
        next();
    },
    "Hello": function(next, name) {
        var returnValue = "Hello " + name;
        next(returnValue);
    },
    "Join": function(next, str1, str2) {
        var returnValue = [str1, str2].join("\n");
        next(returnValue);
    }
};

module.exports = lowlevelKeywords;

Alright! Our low-level keywords look a lot more general compared to the keywords in the basic example!

Now let's create some high-level keywords that give parameters to the low-level keywords and return something themselves. Greet Mikko generates a greeting message for Mikko.

// high-level-keywords.js

var highlevelKeywords = {
    "Create a greeting": [
        "Hello", ["$1"], "=> $helloMikko",
        "Join", ["$helloMikko", "How are you?"], "=> $return"
    ],
    "Greet Mikko": [
        "Create a greeting", ["Mikko"], "=> $greeting",
        "Print", "$greeting"
    ]
};

module.exports = highlevelKeywords;

The syntax is following:

  • Parameters are in an array
  • Parameter value can be either a variable or a primitive, like a number or string
  • $1 stands for the first parameter of the high-level keyword
  • The name of the variable where the return value is saved is a string, that starts with a fat arrow => following by a variable name
  • Variable names always start with dollar sign $

And then the runner.js file, which is mostly the same as in the previous example.

// keywords-with-parameters.js

// Require keyword library
var key = require('keyword');

// Import keyword definitions
key(require('./lowlevel-keywords'));
key(require('./highlevel-keywords'));

key.run("Greet Mikko").then(function() {
    // All done.
});

Now we can run the example by typing

$ node keywords-with-parameters.js

Output

> Hello Mikko
> How are you?

Click here to see the whole example

How does this relate to testing??

Ok, now you've seen how to define and use keywords, but I bet you're eager to know how does this make integration testing awesome!

Web application integration testing is usually done by treating the application as a black box which you interact through a browser. The test cases contain a lot of repeating tasks, such as clicking an element or filling in a form, etc. Keyword.js lets you define these repeating tasks as a general purpose low-level keywords, such as Click, Fill Input, Navigate To URL, etc.

Imaging you're testing an application which has users and the users are able to send messages to each other. Your task is to test sending message from a user Jane to user David.

First thing you have to do is to login as a Jane. This can be done by navigation to the login page (using Navigate To URL). Then you have to fill in user credentials (using Fill Input) and click login button (Click). You can combine all these and create a new high-level keyword, Login as, which takes user name as a parameter.

After that you'll do the messaging stuff, but then you need to assert that David really got the message. So how would you do that? Well, you can use the Login as keyword to login with David's account and see if the message arrived!

I bet you can already see the point of keywords. By defining general purpose low-level keywords, you can easily combine them and create complex high-level keywords that will make your integration testing awesome!

How to interact with the browser?

The library doesn't care how you interact with the browser and what is the browser you're using. You can use for example Zombie, but my favorite is PhantomJS via Selenium Node WebDriver.

If you need to use a 'real' browser (Chrome, Firefox, IE, etc.) WD.js might help you. Haven't tried it, though.

See the Google search without injector below for PhantomJS via WebDriver example.

To run the example, you have to have PhantomJS running with WebDriver on port 4444. To do this, install PhantomJS and type

$ phantomjs --webdriver=4444 &
var key = require('keyword');
var assert = require('assert');
// This example uses WebDriver and PhantomJS
var webdriver = require('selenium-node-webdriver');
var session = webdriver();

// Define keywords
var suite = {

    /***** The main test case *****/
    "Test Google Search": [
        "Google Search For", ["keyword driven testing"], "=> $searchResult",
        "Should Equal", ["$searchResult", "Keyword-driven testing - Wikipedia, the free encyclopedia"]
    ],

    /***** Define high-level keywords ******/
    "Google Search For": [
        "Go To Page", ["http://google.com"],
        "Fill Input By Name", ["q", "$1"],
        "Click Element By Name", ["btnG"],
        "Pick First Search Result", "=> $return"
    ],

    "Pick First Search Result": [
        "Get Text Content Of First Tag", ["h3"], "=> $return"
    ],

    /***** Define low-level keywords ******/
    "Go To Page": function(next, url) {
        session.then(function(driver) {
            console.log("Going to", url);
            return driver.get(url);
        }).done(next);
    },

    "Fill Input By Name": function(next, elementName, text) {
        session.then(function(driver) {
            return driver.
                findElement(driver.webdriver.By.name(elementName)).
                sendKeys(text);
        }).done(next);
    },

    "Click Element By Name": function(next, elementName) {
        session.then(function(driver) {
            return driver.
                findElement(driver.webdriver.By.name(elementName)).click();
        }).done(next);
    },

    "Get Text Content Of First Tag": function(next, elementTagName) {
        session.then(function(driver) {
            return driver.executeScript(function(tag) {
                // This script is run in browser context
                return document.querySelector(tag).textContent;
            }, elementTagName)
            .then(function(firstHit) {
                console.log("The first Google hit:", firstHit);
                return firstHit;
            });
        }).done(next);
    },

    "Should Equal": function(next, a, b) {
        console.log("Should Equal: '" + a + "' and '" + b + "'");
        assert(a === b);
        next();
    }
};

// Load the keywords
key(suite);

console.log();

// Run the keyword
key.run("Test Google Search").then(function() {
    console.log("\nDone.\n");
    session.then(function(driver) {
        driver.quit();
    });
});

Cleaner code with injector

As you can see from the above example, hooking up a WebDriver session brings in some bloat code to each keyword. To get rid of the bloat, injector comes to help.

An injector is a function, that can execute before and after each keyword execution. Injector is also able to inject new parameters to the low-level keyword function.

Here's the Google example with a WebDriver injector. As you can see, the injector adds a driver parameter to each keyword.

var key = require('keyword');
var assert = require('assert');

// Define keywords
var suite = {

    /***** The main test case *****/
    "Test Google Search": [
        "Google Search For", ["keyword driven testing"], "=> $searchResult",
        "Should Equal", ["$searchResult", "Keyword-driven testing - Wikipedia, the free encyclopedia"],
        "Quit"
    ],

    /***** Define high-level keywords ******/
    "Google Search For": [
        "Go To Page", ["http://google.com"],
        "Fill Input By Name", ["q", "$1"],
        "Click Element By Name", ["btnG"],
        "Pick First Search Result", "=> $return"
    ],

    "Pick First Search Result": [
        "Get Text Content Of First Tag", ["h3"], "=> $return"
    ],

    /***** Define low-level keywords ******/
    "Go To Page": function(next, driver, url) {
        console.log("Going to", url);
        driver
        .get(url)
        .then(next);
    },

    "Fill Input By Name": function(next, driver, elementName, text) {
        driver
        .findElement(driver.webdriver.By.name(elementName))
        .sendKeys(text)
        .then(next);
    },

    "Click Element By Name": function(next, driver, elementName) {
        driver
        .findElement(driver.webdriver.By.name(elementName))
        .click()
        .then(next);
    },

    "Get Text Content Of First Tag": function(next, driver, elementTagName) {
        driver.executeScript(function(tag) {
            // This script is run in browser context
            return document.querySelector(tag).textContent;
        }, elementTagName)
        .then(function(firstHit) {
            console.log("The first Google hit:", firstHit);
            return firstHit;
        })
        .then(next);
    },

    "Should Equal": function(next, driver, a, b) {
        console.log("Should Equal: '" + a + "' and '" + b + "'");
        assert(a === b);
        next();
    },
    "Quit": function(next, driver) {
        driver.quit().then(next);
    }
};

// Load the keywords
key(suite);

// Inject webdriver
key.injector(key.webdriver);

console.log();

// Run the keyword
key.run("Test Google Search").then(function() {
    console.log("\nDone.\n");
});

Examples

Basic example:

cd examples/basic
npm install
node basic.js

Keywords with parameters example:

cd examples/keywords-with-parameters
npm install
node keywords-with-parameters.js

Google example (without injector):

cd examples/google
npm install
phantomjs --webdriver=4444 &
node google.js

Google example:

cd examples/google
npm install
phantomjs --webdriver=4444 &
node google.js

Contributing

In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using grunt.

Inspiration

License

Copyright (c) 2013 Mikko Koski
Licensed under the MIT license.

npm loves you