UNPKG

keyword

Version:

Keyword-driven testing library

426 lines (321 loc) 13 kB
# 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. ```javascript // 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. ```javascript // 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`: ```javascript // 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 ```bash $ node basic.js ``` Output ```bash > Hello World > How are you? ``` [Click here to see the whole example](examples/basic) ## 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 ```javascript // 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`. ```javascript // 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. ```javascript // 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 ```bash $ node keywords-with-parameters.js ``` Output ```bash > Hello Mikko > How are you? ``` [Click here to see the whole example](examples/keywords-with-parameters) ## 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](http://zombie.labnotes.org/), but my favorite is [PhantomJS](http://phantomjs.org/) via [Selenium Node WebDriver](https://github.com/WaterfallEngineering/selenium-node-webdriver). If you need to use a 'real' browser (Chrome, Firefox, IE, etc.) [WD.js](https://github.com/admc/wd) might help you. Haven't tried it, though. See the [Google search without injector](examples/google-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 ```bash $ phantomjs --webdriver=4444 & ``` ```javascript 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. ```javascript 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:](examples/basic) ```bash cd examples/basic npm install node basic.js ``` [Keywords with parameters example:](examples/keywords-with-parameters) ```bash cd examples/keywords-with-parameters npm install node keywords-with-parameters.js ``` [Google example (without injector):](examples/google-without-injector) ```bash cd examples/google npm install phantomjs --webdriver=4444 & node google.js ``` [Google example:](examples/google) ```bash 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](https://github.com/cowboy/grunt). ## Inspiration * [Robot Framework](https://code.google.com/p/robotframework/) * [PhantomRobot](https://github.com/datakurre/phantomrobot) * [RoboZombie](https://github.com/mkorpela/RoboZombie) ## License Copyright (c) 2013 Mikko Koski Licensed under the MIT license.