Vidalia¶ ↑
What is Vidalia?¶ ↑
Vidalia uses layers to simplify the creation and maintenance of API and database calls in your automated test suite.
A typical automated test suite can be broken down into layers:
-
A workflow layer on top, which dictates the user’s path through the application. Think in terms of verbs and user stories: “As a user, when I create a new message, I expect it to be sent to the desired recipient.” The workflow layer cares about user actions and results, not about the mechanics of entering or retrieving data.
-
A control layer beneath, which defines the specific interactions with the application. This control layer would be implemented using API calls or direct database calls and cares only about getting data into and out of the application.
Vidalia allows for easy separation of the workflow and control layers by sitting between them. At the control layer, it provides a simple infrastructure for creation and maintenance of API/database calls. At the workflow layer, it provides a set of basic test directives, allowing for easier manipulation of data without having to worry about the details of the underlying interfaces.
How does Vidalia work?¶ ↑
At the control layer, Vidalia allows you to define three basic artifacts: Interfaces, Objects, and Elements
Interface¶ ↑
An Interface is the top layer of the Vidalia artifact hierarchy. It is simply a collection of Objects.
Object¶ ↑
An Object is a collection of data elements that can be manipulated via the defined Interface. Basic Object operations (e.g. create, read, update, delete) or more complex ones can be defined by the user.
Object operations¶ ↑
Operations can be added for an Object to communicate via the Interface. In some cases, the operation will retrieve data via the interface. In others the operation will create or alter data via the interface.
Element¶ ↑
An Element is a single piece of data within an Object. All Elements have a set of directives associated with them to enable easier implementation of your test automation.
Element Directives¶ ↑
Manipulation and verification of each data Element is built around the two primary directives get
and set
:
-
get
- reads and returns the current value of the Element from the Object. Usually performed after data is retreived via an Object operation. -
set
- updates the current value of the Element in preparation for the next operation on the Object.
Once these primary directives are defined, Vidalia puts them together to build the following directives for free:
-
retrieve
- Reads and returns the current value from the Element. Does not log anything. (This is the user-callable version ofget
.) -
update
- Sets a new Element value. Logs the value of the Element both before and after the change for auditing purposes. -
confirm
- Tests the value of the Element, returningtrue
if the input matches the value of the Element andfalse
otherwise. Does not log anything. -
verify
= Tests the value of the Element, raising an exception if the input does not match the value of the Element. Returnstrue
otherwise. Logs successes for auditing purposes.
How do I install Vidalia?¶ ↑
Install the Vidalia gem from the command line as follows:
$ gem install vidalia
How do I get started with Vidalia?¶ ↑
The code examples used below are all taken from the Vidalia repository in the examples/example1
directory. In this directory, we will be automating control of user data in the database defined with by the db_helper.rb
script.
Remember, Vidalia breaks the code into two layers, so we’ll define those layers in different files. The workflow layer will be defined in demo.rb
, but let’s start by using Vidalia to define the control layer in user_object.rb
.
Define the Interface¶ ↑
Our first step is to define an Interface. If the interface has already been defined elsewhere, that definition will be returned. This allows multiple Object definitions (in multiple files) to be added to the same Interface.
app_db = Vidalia::Interface.define(:name => "Application DB")
Define our Object¶ ↑
Now we can define our object. Since we’re working with a user entry in our database, we’ll name it “User”. The definition will indicate that the Object’s parent is our Interface. Additionally, the Object definition will contain a code block to be run whenever this Object is instantiated. A typical Object definition block will instantiate object variables.
user_object = Vidalia::Object.define(:name => "User", :parent => app_db) { singleton_class.class_eval { attr_accessor :id } @id = nil singleton_class.class_eval { attr_accessor :first_name } @first_name = nil singleton_class.class_eval { attr_accessor :last_name } @last_name = nil singleton_class.class_eval { attr_accessor :username } @username = nil }
Define Object operations¶ ↑
In our example, we will define operations to create, retrieve, update, and delete a User object. Note that the use of Objects and Elements will be different depending on the type of operation. To prepare for a “retrieve” operation, the user will set whatever elements are needed to perform the retrieval, after which the operation will be called. To perform an “create” operation, all impacted Elements must be set before the creation.
Let’s start with “create”:
user_object.add_method(:name => "create") { db = SQLite3::Database.open "users.db" sql_string = "INSERT INTO users VALUES(" sql_string << @id.to_s sql_string << "," sql_string << @first_name.to_s sql_string << "," sql_string << @last_name.to_s sql_string << "," sql_string << @username.to_s sql_string << ")" Vidalia.log("Adding a user with DB call \"#{sql_string}\"") db.execute sql_string db.close }
As described above, this operation expects all of the pertinent Elements to already be set. The operation itself just performs a single database operation to create the new Object.
Next we define a “read” operation:
user_object.add_method(:name => "read") { |inhash| db = SQLite3::Database.open "users.db" if inhash[:id] sql_string = "SELECT * FROM users WHERE id = #{inhash[:id]};" Vidalia.log("Reading user ID=#{inhash[:id]} with DB call \"#{sql_string}\"") elsif inhash[:username] sql_string = "SELECT * FROM users WHERE username = #{inhash[:username]};" Vidalia.log("Reading user username=#{inhash[:username]} with DB call \"#{sql_string}\"") end results = db.execute sql_string # Handle empty array results << [nil,nil,nil,nil] if results.size == 0 result = results[0] @id = result.shift @first_name = result.shift @last_name = result.shift @username = result.shift db.close }
Note that this operation doesn’t require any Element data to be pre-set. It will overwrite whatever is stored in the Object with the data returned from the database call.
The “update” operation is similar to “create” in that Element data is already expected to be defined for this operation:
user_object.add_method(:name => "update") { db = SQLite3::Database.open "users.db" sql_string = "Update users SET first_name=\'" sql_string << @first_name.to_s sql_string << "\', last_name=\'" sql_string << @last_name.to_s sql_string << "\', username=\'" sql_string << @username.to_s sql_string << "\' WHERE id=" sql_string << @id.to_s sql_string << ";" Vidalia.log("Updating user ID=#{@id} with DB call \"#{sql_string}\"") db.execute sql_string db.close }
And “delete” requires just an Element or two to accomplish its goal:
user_object.add_method(:name => "delete") { |inhash| db = SQLite3::Database.open "users.db" if inhash[:id] sql_string = "DELETE FROM users WHERE id = #{inhash[:id]};" Vidalia.log("Deleting user ID=#{inhash[:id]} with DB call \"#{sql_string}\"") elsif inhash[:username] sql_string = "DELETE * FROM users WHERE username = #{inhash[:username]};" Vidalia.log("Deleting user username=#{inhash[:username]} with DB call \"#{sql_string}\"") end db.execute sql_string db.close }
Finally, we will complete the Object definition by telling Vidalia how to set and get each Element value. To do this, we have to define the Element objects:
id_element = Vidalia::Element.define(:name => "ID", :parent => user_object) id_element.add_get { |opts| @parent.id } id_element.add_set { |value| @parent.id = value }
For the most part, Element definitions will be this simple. Just name them, associate them with their parent Object, and specify the “get” and “set” directives. With the get
and set
directives defined, Vidalia will use them to build update
, retrieve
, confirm
, and verify
directives for you. We’ll use those in the workflow layer in a bit.
We need three more Elements to complete our example:
first_name_element = Vidalia::Element.define(:name => "First Name", :parent => user_object) first_name_element.add_get { |opts| @parent.first_name } first_name_element.add_set { |value| @parent.first_name = value } last_name_element = Vidalia::Element.define(:name => "Last Name", :parent => user_object) last_name_element.add_get { |opts| @parent.last_name } last_name_element.add_set { |value| @parent.last_name = value } username_element = Vidalia::Element.define(:name => "Username", :parent => user_object) username_element.add_get { |opts| @parent.username } username_element.add_set { |value| @parent.username = value }
And now our Object declaration is complete! Now on to the workflow layer.
Defining Interface, Objects, and Elements using the DSL¶ ↑
Alternatively, the Vidalia DSL can be used to define an interface, its objects, and each object’s elements. The intent of the DSL is to provide a cleaner way to define Vidalia artifacts by minimizing boilerplate code and eliminiating the need to store definitions in variables and pass them to descendants. Here is how the previous examples can be re-written using the DSL:
interface 'Application DB' do object 'User' do init do singleton_class.class_eval { attr_accessor :id } @id = nil singleton_class.class_eval { attr_accessor :first_name } @first_name = nil singleton_class.class_eval { attr_accessor :last_name } @last_name = nil singleton_class.class_eval { attr_accessor :username } @username = nil end create do singleton_class.class_eval { attr_accessor :id } @id = nil singleton_class.class_eval { attr_accessor :first_name } @first_name = nil singleton_class.class_eval { attr_accessor :last_name } @last_name = nil singleton_class.class_eval { attr_accessor :username } @username = nil end read { ... } update { ... } delete { ... } element 'ID' do get { |opts| @parent.id } set { |value| @@parent.id = value } end element 'First Name' { ... } element 'Last Name' { ... } element 'Username' { ... } end end
Invoke the Workflow¶ ↑
To define the workflow layer, let’s edit demo.rb
. First we need to require Vidalia:
require 'vidalia'
Next we want to load our object definition.
require './page'
Before we go any further, we also need to pass a logging routine to Vidalia. This routine just tells Vidalia how to log a given string. For our demonstration, we’ll just print to stdout:
Vidalia::set_logroutine { |logstring| puts logstring }
For the purposes of our example, we need to pull in the database defined by db_helper
. This will use the sqlite3
gem (which is not needed for Vidalia, but needs to be installed to run this test) to build an in-memory database of user data for us to work with. Note that we can now use the log routine we just defined.
Vidalia.log("Building the database for this test") require './db_helper'
Now we can get the Interface we defined in user_object.rb
instantiated into an object:
db = Vidalia::Interface.get("Application DB")
Finally, we can instantiate our user object:
user_object = db.object("User")
Now we get to the good stuff! With all the setup behind us, it’s now pretty straightforward to use our interface. Let’s start by reading a user record. We’ll the user with an ID of 7, who we happen to know is Art VanDelay:
user_object.read(:id => 7)
Now user_object
contains all of the data for the user with ID=7. We can now use the verify
directive to confirm that it contains what we expected it to contain:
user_object.element("First Name").verify "Art" user_object.element("Last Name").verify "VanDelay" user_object.element("Username").verify "realarchitect"
As described above, these directives will log that they have successfully verified the values or they will raise an exception if they fail.
With our user data in hand, now we can manipulate it:
user_object.element("First Name").update "George" user_object.element("Last Name").update "Costanza"
Then save our changes to the database:
user_object.update
Again, we can verify that our update worked with a quick read
from the database and a call to verify
each element of the object:
user_object.read(:id => 7) user_object.element("ID").verify 7 user_object.element("First Name").verify "George" user_object.element("Last Name").verify "Costanza" user_object.element("Username").verify "realarchitect"
As another example, we can call the delete
method to remove the object from the database:
user_object.delete(:id => 7)
And finally, we can demonstrate the confirm
directive by attempting to read
the deleted object and confirming that nothing was returned:
user_object.read(:id => 7) if user_object.element("ID").confirm nil Vidalia.log("Object was successfully deleted!") else raise "Delete didn't work! Object read came back with data!" end
This is a simple example, but hopefully it demonstrates the power and flexibility Vidalia gives you in managing interfaces and objects and using them in the workflow layer of your test automation.