Repository navigation
Tutorial
For this tutorial, we will be using a fake peer-to-peer payment application called Moolah. This application is not yet fully featured, but there is enough functionality in place for us to implement some automated scenarios.
You can download the source code here: https://github.com/lkhoward/moolah
To launch the application, cd to your moolah directory and:
bundle install
bundle exec rackupThe application will be running locally at: http://localhost:9292
The examples in this tutorial will be using RSpec for the testing framework. However, any testing framework will do and should have similar configuration setup. You may need to take additional steps based on your framework of choice (e.g. cucumber scenarios & step definitions).
NOTE: This tutorial assumes that you have a working knowledge of watir-webdriver
In your editor of choice, setup a new project with the following directory structure and files:

Update your Gemfile:
source 'https://rubygems.org' do
gem 'rspec'
gem 'huzzah'
gem 'watir-webdriver'
endNOTE: Although Huzzah is designed to be used with watir-webdriver, the two gems are decoupled. This allow you to choose the version of watir-webdriver that best suits your needs. You need to include both huzzah & watir-webdriver in your Gemfile and require them both in your spec_helper.rb file.
Perform a bundle install to get the gems.
Next, you will need create your Huzzah configuration. This should be done in the RSpec spec_helper.rb file.
require 'huzzah'
require 'watir-webdriver'
Huzzah.configure do |config|
config.path = "#{your_project_path}/lib/huzzah"
config.default_driver = ENV['BROWSER'] ||= 'firefox'
config.environment = ENV['AUTO_ENV'] ||= 'dev'
endSee Configuration for details about the path, default_driver & environment attributes.
See Defining Custom Drivers for more information about using a browser other than Firefox.
Add a file name login_spec.rb under the spec/moolah directory and add the following code:
require_relative '../support/spec_helper'
describe 'Moolah: Login' do
let(:user) { Huzzah::Role.new }
before(:each) do
user.browser.goto('http://localhost:9292')
end
after(:each) do
user.browser.close
end
it 'allows a user to login' do
user.browser.text_field(name: 'email').set('dedric.auer@howe.com')
user.browser.text_field(name: 'password').set('U9HhGbSp')
user.browser.button(value: 'Login').click
expect(user.browser.span(class: 'welcome').when_present.text).to include('Dedric Auer')
end
endExecute your spec and verify that is passes.
bundle exec rspec spec/moolahNext, let's add a second spec that tests a negative login attempt.
require_relative '../support/spec_helper'
describe 'Moolah: Negative Login' do
let(:user) { Huzzah::Role.new }
before(:each) do
user.browser.goto('http://localhost:9292')
end
after(:each) do
user.browser.close
end
it 'displays error message on invalid login attempt' do
user.browser.text_field(name: 'email').set('dedric.auer@howe.com')
user.browser.text_field(name: 'password').set('password')
user.browser.button(value: 'Login').click
expect(user.browser.div(class: 'error').when_present.text).to eql('Username/Password incorrect. Please try again.')
end
endExecute your specs and verify that they pass.
So far we have only used a generic role, but Huzzah allows us to define data about the different roles (or users) that we will need to test our application. We do that by defining a YAML file. Create a new file named dedric.yml under the lib/huzzah/roles directory and add the following information:
---
dev:
:email: 'dedric.auer@howe.com'
:password: 'U9HhGbSp'NOTE: The key dev matches up to the environment defined in our Huzzah.configure block in the spec_helper.rb file. You can define any number of environments in the YAML file by using a different key:
---
dev:
:email: 'dedric.auer@howe.com'
:password: 'U9HhGbSp'
qa:
:email: 'dedric.auer@howe.com'
:password: 'I34TyCxq'Next, update your login_spec.rb file to use this file when we instantiate the role:
let(:user) { Huzzah::Role.new(:dedric) }NOTE: The symbol :dedric must match the name of the YAML file (minus the .yml).
Now we can reference the data from the YAML file in our scenario by accessing the role_data hash:
it 'allows a user to login' do
user.browser.text_field(name: 'email').set(user.role_data[:email])
user.browser.text_field(name: 'password').set(user.role_data[:password])
user.browser.button(value: 'Login').click
expect(user.browser.span(class: 'welcome').when_present.text).to include('Dedric Auer')
endWe can further improve the scenario by adding the expected result (the user's name) to the dedric.yml file:
---
dev:
:email: 'dedric.auer@howe.com'
:password: 'U9HhGbSp'
:full_name: 'Dedric Auer'And then update the scenario to reference it:
expect(role.browser.span(class: 'welcome').when_present.text).to include(user.role_data[:full_name])Execute login_spec.rb again and verify that your scenario still passes.
Next, let's update the negative scenario to also use Dedric:
require_relative '../support/spec_helper'
describe 'Moolah: Negative Login' do
let(:user) { Huzzah::Role.new(:dedric) }
before(:each) do
user.browser.goto('http://localhost:9292')
end
after(:each) do
user.browser.close
end
it 'displays error message on invalid login attempt' do
user.browser.text_field(name: 'email').set(user.role_data[:email])
user.browser.text_field(name: 'password').set(user.role_data[:password])
user.browser.button(value: 'Login').click
expect(role.browser.div(class: 'error').when_present.text).to eql('Username/Password incorrect. Please try again.')
end
endIf we run the spec, the scenario will fail. We want the scenario to have an invalid password, but Dedric has one that is valid. Huzzah provides the option of overwriting (or supplementing) the data from the YAML file. You simply pass a hash of data when you instantiate the Huzzah::Role object. So to provide a bad password we would simply:
let(:user) { Huzzah::Role.new(:dedric, password: 'password') }Dedric's password is now 'password' instead of 'U9HhGbSp'. Run the spec again and it should pass.
There are a couple of important things to note about role_data. First, the data in frozen when a Huzzah::Role is instantiated. It cannot be modified while a scenario is running. This is to prevent data corruption which may cause flapping scenarios. Second, the role_data is available within the Role, Site, Flow, Page & Partial objects. Within your code you can directly reference the @role_data hash. An example of this is below.
The example scenarios that we have built are a very simple, yet poorly implemented. The first thing that needs to be addressed is the hard-coded watir-webdriver locators, so let's take a look at Huzzah's implementation of the page-object model.
Create a new file under your lib/huzzzah/pages directory named login_page.rb and add the following code:
module Moolah
class LoginPage < Huzzah::Page
locator(:email_field) { text_field(name: 'email') }
locator(:password_field) { text_field(name: 'password') }
locator(:login_button) { button(value: 'Login') }
end
endHuzzah uses a Module::Class pattern for organizing Sites & Pages. The module namespace (in this case Moolah) is the Site. The class name (LoginPage) is the Page. Your page-objects will inherit from the Huzzah::Page class, which gives you access to the locator method.
The locator method provides a shorthand way of defining and organizing how your test scenarios identify elements within the DOM. It takes two arguments, a name and a block. The name is used to dynamically define a method with that name, which executes the block of code (the second argument).
For example, the locator:
locator(:email_field) { text_field(name: 'email') }Is equivalent to:
def email_field
text_field(name: 'email')
endLet's refactor the positive login scenario to use our new page-object:
it 'allows a user to login' do
user.moolah.login_page.email_field.set(user.role_data[:email])
user.moolah.login_page.password_field.set(user.role_data[:password])
user.moolah.login_page.login_button.click
expect(role.browser.span(class: 'welcome').when_present.text).to include(user.role_data[:full_name])
endWith the addition of the page-object, the syntax now becomes role.site.page.element.action.
At this point, it is important to note that we have done nothing but define a class. Huzzah 'wires things together' for you auto-magically. In fact, the only thing you ever have to instantiate is a Huzzah::Role object. In the above example, the moolah and login_page methods are dynamically generated based on the module namespace and class name of your page object (converted to snake case).
Our expectation still has a watir locator in it, so let's add a second page-object to represent the Moolah User Account page. Create a new file named user_account.rb under you 'pages' directory.
module Moolah
class UserAccount < Huzzah::Page
locator(:welcome_message) { span(class: 'welcome') }
end
endAnd update the expectation to use the new page object.
expect(user.moolah.user_account.welcome_message.when_present.text).to include(user.role_data[:full_name])There are a few other important things to note about the locator method. First, within the block of code, you do not need to reference the browser. Huzzah knows that you are calling a method on the browser, which reduces boilerplate coding. Second, for clarity, Huzzah will NOT let you use a name for your locator that matches a name of any method found in the Watir::Container class (e.g. table, link, etc.). Lastly, the block of code can be used for anything (it's just Ruby code after all), but we highly recommend that you only use it for locating elements. If you need to do something else, write a method for it.
Our scenario is much cleaner and more readable now, but we can make it better. Let's add a method to login_page.rb that will handle the login attempt:
module Moolah
class LoginPage < Huzzah::Page
locator(:email_field) { text_field(name: 'email') }
locator(:password_field) { text_field(name: 'password') }
locator(:login_button) { button(value: 'Login') }
def login
email_field.set(@role_data[:email])
password_field.set(@role_data[:password])
login_button.click
end
end
endRefactored scenario:
it 'allows a user to login' do
user.moolah.login_page.login
expect(user.moolah.user_account.welcome_message.when_present.text).to include(user.role_data[:full_name])
endOur scenario is now smart enough to use the @role_data to login into the application. We just need to ensure that we provide credentials for all of our Huzzah::Role objects.
As our scenario stands now, it is hard-coded to run against the Moolah dev environment. However, it is a common problem that we need our scenarios to be able to run in multiple environments like dev, qa, pre-prod, etc. Huzzah solves that problem through site configs.
Site Configs are simply YAML files that contain information about your site and each environment you would like to be able to run your scenarios against. When you created your Huzzah config in your spec_helper.rb file, you set a variable named 'environment'. The value of that variable is used to load the data from the site for the specified environment.
Under your lib/huzzah/sites directory, create a new file named moolah.yml and add the following code:
---
dev:
:url: 'http://localhost:9292'NOTE: In order for Huzzah to be able to 'wire up' things correctly, the file name of your YAML file must be the snake case version of the module namespace in your page-objects (e.g. module Moolah == moolah.yml).
In the above example, we only have one environment defined (dev) and only one attribute (url). Like Role YAML files, you can have any number of environments defined and you can put any information that you need about your site in this file (e.g. database config). The url attribute is special. It is used to navigate the browser to the site:
user.visit(:moolah)
or
user.visit('moolah')
or
user.moolah.visitAny of the above method calls will navigate the browser to the URL of the environment specified in your Huzzah config.
Update your spec scenario to use the visit method:
before(:each) do
user.moolah.visit
endSee Sites for more information.
Flows are how Huzzah defines user interactions across multiple pages of your application or across pages of multiple sites. In order to demonstrate how flows work, we will need to dive a little deeper into the Moolah application. Let's create a send_payment_spec.rb with a scenario that will send a payment to another user:
require_relative '../support/spec_helper'
describe 'Moolah: Send Payment' do
let(:user) { Huzzah::Role.new(:dedric) }
before(:each) do
user.moolah.visit
end
after(:each) do
user.browser.close
end
it 'allows a user to login' do
user.moolah.login_page.login
user.moolah.user_account.send_payment('carey.braun@okon.co', '5.00')
expect(user.moolah.user_account.sent_message).to be_present
end
endAdd locators to the user_account.rb page for the Send Payment form elements and add method to send a payment:
module Moolah
class UserAccount < Huzzah::Page
locator(:welcome_message) { span(class: 'welcome') }
locator(:email_field) { text_field(name: 'email') }
locator(:amount_field) { text_field(name: 'amount') }
locator(:send_button) { button(value: 'Send Payment') }
locator(:sent_message) { div(text: 'Payment Sent.') }
def send_payment(user, amount)
email_field.when_present.set(user)
amount_field.set(amount)
send_button.click
end
end
endRun your spec to ensure that the new scenario passes.
Next, let's create a flow that will login to Moolah and send a payment to another user. Under your flows directory, create a new file named payments.rb and add the flowing code:
class Payment < Huzzah::Flow
def send_to(user, amount)
moolah.login_page.login
moolah.user_account.send_payment(user, amount)
end
endHuzzah uses the class name of the flow (Payment) to generate a method that provides access to the methods that you defined within the flow. In this case, class Payment becomes method payment within the Huzzah::Role object.
Update the scenario to use the flow:
it 'allows a user to send a payment' do
user.payment.send_to('carey.braun@okon.co', '5.00')
expect(user.moolah.user_account.sent_message).to be_present
endSee Flows for more information.
Partials are simply Pages that are created under the partials directory.
The idea is that partials will be used to keep you code DRY by defining locators that are common to multiple pages. They could also be used to break complex pages into more manageable components.
The syntax for defining a partial is exactly like a page:
module Moolah
class Header < Huzzah::Page
locator(:logout) { img(class: 'logout') }
end
endThe only difference is how they are referenced. You link a partial to a page using the include_partial method:
module Moolah
class UserAccount < Huzzah::Page
include_partial(Moolah::Header)
locator(:welcome_message) { span(class: 'welcome') }
locator(:email_field) { text_field(name: 'email') }
locator(:amount_field) { text_field(name: 'amount') }
locator(:send_button) { button(value: 'Send Payment') }
locator(:sent_message) { div(text: 'Payment Sent.') }
def send_payment(user, amount)
email_field.when_present.set(user)
amount_field.set(amount)
send_button.click
end
end
endThe include_partial method dynamically defines a method using the name of the Partial class. In this case, the method would be named header and would be called like this:
user.moolah.user_account.header.logout.clickThe syntax for partials is role.site.page.partial.element.action.
NOTE: The partial's module namespace is ignored when the method is generated and it does not need to match the module namespace of the page.
See Partials for more information.
Add a second user role carey.yml:
---
dev:
:email: 'carey.braun@okon.co'
:password: '69MjL8E8'Create a new scenario that involves both users:
require_relative '../support/spec_helper'
describe 'Moolah: Send Payment' do
let(:dedric) { Huzzah::Role.new(:dedric) }
let(:carey) { Huzzah::Role.new(:carey) }
before(:each) do
dedric.moolah.visit
carey.moolah.visit
end
after(:each) do
dedric.browser.close
carey.browser.close
end
it 'allows a user to view recent activity' do
dedric.payment.send_to('carey.braun@okon.co', '5.00')
carey.navigate.to_to_account_activity
expect(carey.moolah.account_activity.most_recent_transaction).to include(dedric.role_data[:full_name].upcase)
end
endAdd a flow to login and navigate to the user's transactions page:
class Navigate < Huzzah::Flow
def to_account_activity
moolah.login_page.login
moolah.user_account.view_account_activity.click
end
endAdd the locator form the View Account Activity link to the UserAccount page:
locator(:view_account_activity) { link(text: 'View Account Activity >>') }Add the AccountActivity page:
module Moolah
class AccountActivity < Huzzah::Page
locator(:activities) { divs(class: 'activity') }
def most_recent_transaction
activities.last.text
end
end
end