Sahi Framework - Scenarios using Java
abstract
The Sahi Framework allows testers to write their testcases in a Spreadsheet (Excel like) interface and run it from Sahi.
Often a testing team consists of a mix of subject matter experts, some manual testers and testers with some automation experience. Writing tests in the language of the business allows all stake holders to participate and derive value out of the automation process.
Folder structure:
|- Sahi
|- userdata
|-scripts
|-java
|- src
|- classes
|- lib
|
|
|
|
When the above folder structure is followed, on invoking java methods from editor, Sahi will be able to:
- pick those methods for non-distributed playback.
- distribute those methods for distributed playback.
info
- One needs to add
sahi.jar
, located at sahi/lib
, to the classpath.
- Name Source folder as
src
.
- Name Default output folder as
classes
.
- Name Library folder as
lib
.
- To populate all
public
methods of class in script editor's auto-complete dropdown, follow either of these steps:
- Java Class should implement
SahiMarkerInterface
.
Add an import statement import com.sahipro.lang.java.client.SahiMarkerInterface;
.
- Add fully qualified name of the class in
sahi/userdata/scripts/java/exposed_classes.txt
, separated by new line.
(This file supports single line comment. Use #
or //
for the same.)
- Declare instance variables of Sahi Capability required. For example:
com.sahipro.lang.java.client.Browser
, com.sahipro.lang.java.client.Windows
, com.sahipro.lang.java.client.Applet
and com.sahipro.lang.java.client.JavaApplication
.
Declare a parameterized contructor which initializes these variables.
For example: take class name as UserModule
Browser browser = null;
Windows window = null;
JavaApplication javaApplication = null;
public UserModule(SahiCapabilities sahiCapabilities) {
this.b = sahiCapabilities.getBrowser(); //to retrieve Browser instance
this.window = sahiCapabilities.getWindows(); //to retrieve Windows instance
this.javaApplication = sahiCapabilities.getJavaApplication(); //to retrieve JavaApplication instance
}
package demo.training;
import com.sahipro.lang.java.client.Applet;
import com.sahipro.lang.java.client.Browser;
import com.sahipro.lang.java.client.JavaApplication;
import com.sahipro.lang.java.client.SahiCapabilities;
import com.sahipro.lang.java.client.SahiMarkerInterface;
import com.sahipro.lang.java.client.Windows;
public class UserModule implements SahiMarkerInterface {
Browser b = null;
Windows w = null;
JavaApplication javaApplication = null;
public UserModule(SahiCapabilities sahiCapabilities) {
this.b = sahiCapabilities.getBrowser();
this.w = sahiCapabilities.getWindows();
this.javaApplication = sahiCapabilities.getJavaApplication();
}
}
A sample Java Class looks like this:
package demo.training;
import com.sahipro.lang.java.client.Browser;
import com.sahipro.lang.java.client.SahiCapabilities;
import com.sahipro.lang.java.client.SahiMarkerInterface;
public class UserModule implements SahiMarkerInterface {
Browser b = null;
public UserModule(SahiCapabilities sahiCapabilities) {
this.b = sahiCapabilities.getBrowser();
}
public void login(String username, String password) {
String baseURL = "http://sahitest.com/";
b.navigateTo(baseURL + "/demo/training/login.htm");
b.textbox("user").setValue(username);
b.password("password").setValue(password);
b.click(b.submit("Login"));
}
public void addBooks(int java, int ruby, int python) {
b.setValue(b.textbox("q").near(b.cell("Core Java")), java+"");
b.setValue(b.textbox("q").near(b.cell("Ruby for Rails")), ruby+"");
b.setValue(b.textbox("q").near(b.cell("Python Cookbook")), python+"");
b.click(b.button("Add"));
}
public void verifyTotal(int total) {
b.areEqual(total+"", b.textbox("total").getValue());
}
public void logout() {
b.click(b.button("Logout"));
}
public void verifyNotLoggedIn() {
b.assertExists(b.textbox("user"));
}
public void verifyErrorMessage(String message) {
b.assertVisible(b.div("errorMessage"));
b.assertText(b.div("errorMessage"), message);
}
public void addBooksAndVerifyTotal(int java, int ruby, int python, int total){
addBooks(java, ruby, python);
verifyTotal(total);
}
}
Use
$<className>.<functionName>
, to invoke Java methods. First letter of the java class will be in lower case.
For example : Class
UserModule
has method
login(String userName, String password)
. To invoke this method use
$userModule.login
, and pass the necessary arguments
- Logs
- Java code executed on the browser are automatically logged by Sahi.
- Clicking on steps in the logs will show the stack trace.
- Java code can be executed across machines as a distributed playback.
- Sahi and Java methods can be invoked concurrently from the same
script/scenario file
.
- No need to restart Sahi when classes or jars are modified. Sahi will invoke the modified code.
- Assertions in
Java Driver
are analogous to the one in Sahi Scripts
. Same implicit wait-retry mechanism
is followed.
Script execution does not stop on assertion failure.
- Declaring Java class with same name in different packages.
- Playback of script, invoking Java methods, from a
URL
.
A simple Scenario with Java looks like this:
TestCase |
Key Word |
Argument1 |
Argument2 |
Argument3 |
|
importJava |
"demo.training.UserModule" |
|
|
|
|
|
|
|
|
|
|
|
|
Check shopping cart total |
[Documentation] |
Smoke test for add books |
|
|
$userModule.login |
"test" |
"secret" |
|
|
$userModule.addBooks |
3 |
2 |
1 |
|
$userModule.verifyTotal |
1640 |
|
|
|
$userModule.logout |
|
|
|
|
|
|
|
|
|
|
|
|
|
Test login error message |
[Documentation] |
Checks Invalid login message |
|
|
$userModule.login |
"test" |
"bad password" |
|
|
$userModule.verifyNotLoggedIn |
|
|
|
|
$userModule.verifyErrorMessage |
"Invalid username or password" |
|
These tests talk mostly in the language of the business (also called a Domain Specific Language or DSL for that business), and hide away all the implementation details of clicking buttons and populating textboxes.
Starting from Sahi Pro 6.1.0, a new format as shown below is introduced. This allows for adding additional columns. The example below shows two additional columns Comments and Tags added to the file.
Comments |
Tags |
TestCase |
Key Word |
Argument1 |
Argument2 |
Argument3 |
Load the function library file |
|
|
importJava |
"demo.training.UserModule" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
First Testcase |
smoke, admin |
Check shopping cart total |
[Documentation] |
Smoke test for add books |
|
|
|
|
$userModule.login |
"test" |
"secret" |
|
|
|
|
$userModule.addBooks |
3 |
2 |
1 |
|
|
|
$userModule.verifyTotal |
1640 |
|
|
|
|
|
$userModule.logout |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Second Testcase |
all, smoke, user |
Test login error message |
[Documentation] |
Checks Invalid login message |
|
|
|
|
$userModule.login |
"test" |
"bad password" |
|
|
|
|
$userModule.verifyNotLoggedIn |
|
|
|
|
|
|
$userModule.verifyErrorMessage |
"Invalid username or password" |
|
At the time of execution from Script Editor or commandline, you can provide the expression for evaluating tags using binary operators. In above case, if you want to execute testcases for admin and user, the tag can be "admin || user".
info
Additional columns can only be added before TestCase column.
A column with heading as "Tags" have a special meaning and will allow you to select the tags during execution.
The implementation details are moved into an included Java class, which is linked to this Scenario file via the initial statement.
| importJava | "demo.training.UserModule" |
The code in
demo.training.UserModule
is given below:
package demo.training;
import com.sahipro.lang.java.client.Browser;
import com.sahipro.lang.java.client.SahiCapabilities;
import com.sahipro.lang.java.client.SahiMarkerInterface;
public class UserModule implements SahiMarkerInterface {
Browser b = null;
public UserModule(SahiCapabilities sahiCapabilities) {
this.b = sahiCapabilities.getBrowser();
}
public void login(String username, String password) {
String baseURL = "http://sahitest.com/";
b.navigateTo(baseURL + "/demo/training/login.htm");
b.textbox("user").setValue(username);
b.password("password").setValue(password);
b.click(b.submit("Login"));
}
public void addBooks(int java, int ruby, int python) {
b.setValue(b.textbox("q").near(b.cell("Core Java")), java+"");
b.setValue(b.textbox("q").near(b.cell("Ruby for Rails")), ruby+"");
b.setValue(b.textbox("q").near(b.cell("Python Cookbook")), python+"");
b.click(b.button("Add"));
}
public void verifyTotal(int total) {
b.areEqual(total+"", b.textbox("total").getValue());
}
public void logout() {
b.click(b.button("Logout"));
}
public void verifyNotLoggedIn() {
b.assertExists(b.textbox("user"));
}
public void verifyErrorMessage(String message) {
b.assertVisible(b.div("errorMessage"));
b.assertText(b.div("errorMessage"), message);
}
}
Executing the Scenario file is no different from executing a Sahi script.
On execution, Sahi generates logs showing success or failure. Logs are visible from the "Logs" link in Playback tab.
Logs can also be accessed via http://localhost:9999/logs
A sample log is shown below.
Refer to
sahi/userdata/scripts/sahitests/framework folder for some examples.
Test Case | Key word | Argument 1 | Argument 2 | Argument 3 |
| | | | |
| importJava | "demo.training.UserModule" | | |
| | | | |
Test Case One | Step One | Param1 | Param2 | |
| Step Two | Param3 | | |
| | | | |
Test Case Two | Step One | 25 | "age" | |
| Step Two | Param5 | | |
| | | | |
Test Case Three | [Documentation] | Some description about the test case | | |
| Step One | 25 | "age" | |
// | Step Two | Param5 | | |
| Step Two | Param6 | | |
| | | | |
Spaces will be removed from keywords and corresponding functions invoked.
The rules for writing the Scenario are as follows
The first line should be populated with
Test Case | Key word | Argument 1 | Argument 2 | Argument 3
The names of the columns are not important, but they should not be left blank
If the first column is populated, a new test case is started.
The second column holds keywords. Keywords are mapped to functions in the included Sahi script/imported Java Class.
They can be user defined functions or Sahi APIs themselves
For example,
| $userModule.login | "test" | "secret" |
in the Scenario, maps to the java method call, defined in
demo.training.UserModule
login("test", "secret");
The Scenario file also supports variables, eg.
| $amount= | 1000 | | |
| $userModule.verifyAmount | $amount | | |
Eg. To get the value returned by function
createUserInGroup
:
Using [ReturnValue]
| $userModule.createUserInGroup | "My name" | "My group" | |
| $userId= | [ReturnValue] | | |
| $userModule.verifyUserCreated | $userId | "My name" | "My group" |
info[ReturnValue]
is a keyword to access return value of function executed in previous step. Added since Sahi Pro 6.1.0
Inline declaration
| $userId=$userModule.createUserInGroup | "My name" | "My group" | |
| $userModule.verifyUserCreated | $userId | "My name" | "My group" |
Inline as code
| $java= | 3 | | |
| $userModule.addBooks | $java | 2 | 1 |
Different test cases may need the same steps to be executed before and after.
For example, one may need to login before and logout after each test case.
This can be accomplished through global SetUp and TearDown blocks.
TearDown will be called inspite of any errors or failures in the testcase.
infoThe [Global] keyword is mandatory and defines these [Setup] and [Teardown] methods for all testcase blocks.
[Global] | [SetUp] | | | |
| _log | "In Global Setup" | | |
| $userModule.login | "test" | "secret" | |
| | | | |
| [TearDown] | | | |
| $userModule.logout | | | |
| _log | "In Global Teardown" | | |
| | | | |
Verify books total | [Documentation] | Check once | | |
| $userModule.addBooks | 3 | 2 | 1 |
| $userModule.verifyTotal | 1650 | | |
| | | | |
Verify books again | [Documentation] | Check again | | |
| $userModule.addBooks | 3 | 2 | 2 |
| $userModule.verifyTotal | 2000 | | |
This will execute as:
| _log | "In Global Setup" | | |
| $userModule.login | "test" | "secret" | |
| $userModule.addBooks | 3 | 2 | 1 |
| $userModule.verifyTotal | 1650 | | |
| $userModule.logout | | | |
| _log | "In Global Teardown" | | |
| | | | |
| | | | |
| _log | "In Global Setup" | | |
| $userModule.login | "test" | "secret" | |
| $userModule.addBooks | 3 | 2 | 2 |
| $userModule.verifyTotal | 2000 | | |
| $userModule.logout | | | |
| _log | "In Global Teardown" | | |
Data Driven Example | [Keyword] | $userModule.addBooksAndVerifyTotal | | | |
| | | | | |
| [SetUp] | | | | |
| $userModule.login | "test" | "secret" | | |
| | | | | |
| [TearDown] | | | | |
| $userModule.logout | | | | |
| | | | | |
| [Documentation] | java | ruby | python | total |
| [Data] | 3 | 2 | 1 | 1650 |
| | 4 | 5 | 0 | 2100 |
| | 0 | 1 | 9 | 3350 |
This roughly translates to:
| $userModule.login | "test" | "secret" | | |
| $userModule.addBooksAndVerifyTotal | 3 | 2 | 1 | 1650 |
| $userModule.logout | | | | |
| | | | | |
| $userModule.login | "test" | "secret" | | |
| $userModule.addBooksAndVerifyTotal | 4 | 5 | 0 | 2100 |
| $userModule.logout | | | | |
| | | | | |
| $userModule.login | "test" | "secret" | | |
| $userModule.addBooksAndVerifyTotal | 0 | 1 | 9 | 3350 |
| $userModule.logout | | | |
| | | | | |
Normally, parameter data is passed inline to keywords/functions in scenario files.
However, one may want to keep the parameter data in a separate file for easier maintenance.
Sahi Pro 6.2 adds the ability to represent data in external files or database and allows an easy way of accessing such externalized data.
There can be three different ways of accessing external data in Sahi Scenarios:
1) Through CSV file by using _readCSVFile api
| D1=_readCSVFile | sample_data.csv | |
2) Through excel file by using _readExcelFile api
| D1=_readExcelFile | sample_data.xls | |
3) Through database by using _getDB api
| $db= | _getDB($jdbcDriver, $jdbcURL, "", "") | |
| $sql= | "SELECT * FROM EXTERNALDATA" | |
| D2= | $db.selectWithHeader($sql) | |
To understand how this data can be used in scenario files.
Consider a situation where a new user is to be created and added to the database. Each user has four properties lets say firstname, lastname, age, gender.
One wants to pass these parameters as data in a function called "addUser()", which takes these four parameters in the same sequence as mentioned above.
public void addUser(String firstname, String lastname, int age, String gender) {
...
}
Now, lets have a look at how the external data can look like.
Case 1 |
data1 |
Shyam |
Sundar |
11 |
male |
|
|
|
|
|
|
Case 2 |
data2 |
firstname |
lastname |
age |
gender |
|
|
Shyam |
Sundar |
11 |
male |
|
|
Jack |
Sparrow |
21 |
male |
|
|
|
|
|
|
Case 3 |
data3 |
age |
gender |
firstname |
lastname |
|
|
11 |
male |
Shyam |
Sundar |
|
|
21 |
male |
Jack |
Sparrow |
|
|
|
|
|
|
Case 4 |
data4 |
firstname |
lastname |
age |
gender |
|
|
Jack |
Sparrow |
21 |
male |
|
|
|
|
|
|
|
|
Shyam |
Sundar |
11 |
male |
|
|
|
|
|
|
From database:
Case 5 |
firstname |
lastname |
age |
gender |
|
|
Jack |
Sparrow |
21 |
male |
|
|
Shyam |
Sundar |
11 |
male |
|
Here data1 is a 1-dimensional array, which can be directly accessed as [D1:data1] and passed to the addUser function.
TC:addUser |
[Documentation] |
Add User to the database |
|
$userModule.addUser |
[D1:data1] |
Or
TC:addUser |
[Keyword] |
$userModule.addUser |
|
[Data] |
[D1:data1] |
Here data2 is a 2-dimensional array and column sequence is same as that required by the function. So it can either be passed directly as [D1:data2] or each parameter in the same sequence as required.
TC:addUser |
[Keyword] |
$userModule.addUser |
|
|
|
|
[Data] |
[D1:data2] |
|
|
|
// or |
[Data] |
[D1:data2:firstname] |
[D1:data2:lastname] |
[D1:data2:age] |
[D1:data2:gender] |
Here also data3 is a 2-dimensional array but sequence of the columns is not the same as the function. So here we
can not
pass it directly as [D1:data3].
TC:addUser |
[Keyword] |
$userModule.addUser |
|
|
|
|
[Data] |
[D1:data3:firstname] |
[D1:data3:lastname] |
[D1:data3:age] |
[D1:data3:gender] |
This is same as the Case2. Empty rows will be ignored.
This is the case when we fetch data from any database using _getDB. There is no key like "data1" here so each column can be accessed as [D2::columnName].
TC:addUser |
[Keyword] |
$userModule.addUser |
|
|
|
|
[Data] |
[D2::firstname] |
[D2::lastname] |
[D2::age] |
[D2::gender] |