# Kookaburra # Kookaburra is a framework for implementing the [Window Driver] [Window Driver] pattern in order to keep acceptance tests maintainable. ## Installation ## Kookaburra is available as a Rubygem and [published on Rubygems.org] [Kookaburra Gem], so installation is trivial: gem install kookaburra If you're using [Bundler](http://gembundler.com/) for your project, just add the following: group :development, :test do gem 'kookaburra' end ## Setup ## Kookaburra itself abstracts some common patterns for implementing the Window Driver pattern for tests of Ruby web applications built on [Rack] [Rack]. You will need to tell Kookaburra which classes contain the specific Domain Driver implementations for your application as well as which driver to use for running the tests (currently only tested with [Capybara] [Capybara]). The details of setting up your Domain Driver layer are discussed below, but in general you will need the following in a locations such as `lib/my_application/kookaburra.rb` (replace `MyApplication` with a module name suitable to your actual application: module MyApplication module Kookaburra ::Kookaburra.adapter = Capybara # Note: the following assigned classes are defined under your # application's namespace, e.g. MyApplication::Kookaburra::APIDriver ::Kookaburra.api_driver = APIDriver ::Kookaburra.given_driver = GivenDriver ::Kookaburra.ui_driver = UIDriver ::Kookaburra.test_data_setup do provide_collection :accounts # See section on Test Data for more examples of what can go here. end end end ### RSpec ### For [RSpec] [RSpec] integration tests, just add the following to `spec/support/kookaburra_setup.rb`: require 'my_application/kookaburra' RSpec.configure do |c| c.include(Kookaburra, :type => :request) end ### Cucumber ### For [Cucumber] [Cucumber], add the following to `features/support/kookaburra_setup.rb`: require 'my_application/kookaburra' Kookaburra.adapter = Capybara World(Kookaburra) Before do # Ensure that there isn't state-leakage between scenarios kookaburra_reset! end This will cause the #api, #given and #ui methods will be available in your Cucumber step definitions. ## Defining Your Testing DSL ## Kookaburra attempts to extract some common patterns that make it easier to use the Window Driver pattern along with various Ruby testing frameworks, but you still need to define your own testing DSL. An acceptance testing stack using Kookaburra has the following layers: 1. The **Business Specification Language** (Cucumber scenarios or other spcification documents) 2. The **Test Implementation** (Cucumber step definitions, RSpec example blocks, etc.) 3. The **Domain Driver** (Kookaburra::GivenDriver and Kookaburra::UIDriver) 4. The **Window Driver** (Kookaburra::UIDriver::UIComponent) 5. The **Application Driver** (Capybara and Rack::Test) ### The Business Specification Language ### The business specification language consists of the highest-level descriptions of a feature that are suitable for sharing with the non/less-technical stakeholders on a project. Gherkin is the external DSL used by Cucumber for this purpose, and you might have the following scenario defined for an e-commerce application: # purchase_items_in_cart.feature Feature: Purchase Items in Cart Scenario: Using Existing Billing and Shipping Information Given I have an existing account And I have previously specified default payment options And I have previously specified default shipping options And I have an item in my shopping cart When I sign in to my account And I choose to check out Then I see my order summary And I see that my default payment options will be used And I see that my default shipping options will be used Note that the scenario is focused on business concepts versus interface details, i.e. you "choose to check out" rather than "click on the checkout button". If for some reason your e-commerce system was going to be a terminal application rather than a web application, you would not need to change this scenario at all, because the actual business concepts described would not change. ### The Test Implementation ### The Test Implementation layer exists as the line in between the Business Specification Language and the Domain Driver, and it includes Cucumber step definitions, RSpec example blocks, Test::Unit tests, etc. At this layer, your code orchestrates calls into the Domain Driver to mimic user interactions under various conditions and make assertions about the results. **Test assertions always belong within the test implementation layer.** Some testing frameworks such as RSpec add methods like `#should` to `Object`, which has the effect of poisoning the entire Ruby namespace with these methods---if you are using RSpec, you can call `#should` anywhere in your code and it will work when RSpec is loaded. Do not be tempted to call a testing library's Object decorators anywhere outside of your test implementation (such as within `UIDriver` or `UIComponent` subclasses.) Doing so will tightly couple your Domain Driver and/or Window Driver implementation to a specific testing library. If you must make some type of assertion within the Domain Driver layer, a better approach is to simply raise an exception with an informative error message when some desired condition is not met. Kookaburra provides its own `#assert` method for this purpose. You may use it directly or build your own custom assertions using it as a base. However, this method should be used only for the purpose of short-circuiting your Domain Driver with an informative error message, not to test the results of your operations as you would at the test implementation layer. Given the Cucumber scenario above, here is how the test implementation layer might look: # step_definitions/various_steps.rb Given "I have an existing account" do given.existing_account(:my_account) end Given "I have previously specified default payment options" do given.default_payment_options_specified_for(:my_account) end Given "I have previously specified default shipping options" do given.default_shipping_options_specified_for(:my_account) end Given "I have an item in my shopping cart" do given.an_item_in_my_shopping_cart(:my_account) end When "I sign in to my account" do ui.sign_in(:my_account) end When "I choose to check out" do ui.choose_to_check_out end Then "I see my order summary" do ui.should be_displaying_order_summary end Then "I see that my default payment options will be used" do ui.should be_displaying_account_default_payment_options_in_order_summary end Then "I see that my default shipping options will be used" do ui.should be_displaying_account_default_shipping_options_in_order_summary end The step definitions contain neither explicitly shared state (instance variables) nor any logic branches; they are simply wrappers around calls into the Domain Driver layer. There are a couple of advantages to this approach. First, because step definitions are so simple, it isn't necessary to force *Very Specific Wording* on the business analyst/product owner who is writing the specs. For instance, if she writes "I see a summary of my order" in another scenario, it's not a big deal to have the following in your step definitions (as long as the author of the spec confirms that they really mean the same thing): Then "I see my order summary" do ui.should be_displaying_order_summary end Then "I see a summary of my order" do ui.should be_displaying_order_summary end The step definitions are nothing more than a natural language reference to an action in the Domain Driver; there is no overwhelming maintenance cost to the slight duplication, and it opens up the capacity for more readable Gherkin specs. The fewer false road blocks you put between your product owner and a written specification, the easier it becomes to ensure her participation in this process. The second advantage is that by pushing all of the complexity down into the Domain Driver, it's now trivial to reuse the exact same code in developer-centric integration tests. This ensures you have parity between the way the automated acceptance tests run and any additional testing that the development team needs to add in. Using RSpec, the test implementation would be as follows: # spec/integration/purchase_items_in_cart_spec.rb describe "Purchase Items in Cart" do example "Using Existing Billing and Shipping Information" do given.existing_account(:my_account) given.default_payment_options_specified_for(:my_account) given.default_shipping_options_specified_for(:my_account) given.an_item_in_my_shopping_cart(:my_account) ui.sign_in(:my_account) ui.choose_to_check_out ui.order_summary.should be_visible ui.should be_displaying_account_default_payment_options_in_order_summary ui.should be_displaying_account_default_shipping_options_in_order_summary end end ### The Domain Driver ### The Domain Driver layer is where you build up an internal DSL that describes the business concepts of your application at a fairly high level. It consists of three top-level drivers: the `APIDriver` (available via `#api`) for interacting with your application's external API, the `GivenDriver` (available via `#given`) which really just wraps the `APIDriver` and is used to set up state for your tests, and the UIDriver (available via `#ui`) for describing the tasks that a user can accomplish with the application. #### Test Data #### `Kookaburra::TestData` is the component via which the `GivenDriver` and the `UIDriver` share information. For instance, if you create a user account via the `GivenDriver`, you would store the login credentials for that account in the `TestData` instance, so the UIDriver knows what to use when you tell it to `#sign_in`. This is what allows the Cucumber step definitions to remain free from explicitly shared state. The `TestData` class can be configured to contain both collections of test data as well as default data that can be used as a starting point for creating new resources in the application. To configure `TestData`, call `Kookaburra.test_data_setup` with a block (usually in your `lib/my_application/kookaburra.rb` file): module MyApplication module Kookaburra # ... ::Kookaburra.test_data_setup do provide_collection :animals set_default :animal, :name => 'horse' :size => 'large', :number_of_legs => 4 end end end Then, in any context where you have an instance of `TestData` (such as in `GivenDriver` or `UIDriver`), you can add/retrieve items to/from collections and access default data: class MyApplication::Kookaburra::GivenDriver < Kookaburra::GivenDriver def existing_account(nickname) default_account_data = test_data.default(:account) # do something to create account in application # ... # make the details of the new account available to the rest of the test test_data.set_accounts(nickname, account) end end class MyApplication::Kookaburra::UIDriver < Kookaburra::UIDriver def sign_in(account_nickname) # pull stored account details from TestData account_info = test_data.fetch_accounts(account_nickname) # do something to log in using that account_info end end #### API Driver #### The `Kookaburra::APIDriver` is used to interact with an application's external web services API. You tell Kookaburra about your API by creating a subclass of `Kookaburra::APIDriver` for your application: # lib/my_application/kookaburra/api_driver.rb class MyApplication::Kookaburra::APIDriver < Kookaburra::APIDriver def create_account(account_data) post_as_json 'Account', 'api/v1/accounts', :account => account_data hash_from_response_json[:account] end end #### Given Driver #### The `Kookaburra::GivenDriver` is used to create a particular "preexisting" state within your application's data and ensure you have a handle to that data (when needed) prior to interacting with the UI. Like the `APIDriver`, you will create a subclass of `Kookaburra::GivenDriver` in which you will create part of the Domain Driver DSL for your application: # lib/my_application/kookaburra/given_driver.rb class MyApplication::Kookaburra::GivenDriver < Kookaburra::GivenDriver def existing_account(nickname) # grab the default account details and add a unique username and # password account_data = test_data.default(:account) account_data[:username] = "test-user-#{`uuidgen`.strip}" account_data[:password] = account_data[:username] + "-password" # use the API to create the account in the application account_details = api.create_account(account_data) # merge in the password (since API doesn't return it) and store details # in the TestData instance account_details.merge(:password => account_data[:password]) test_data.set_accounts(nickname, account_details) end end Although there is nothing that actually *prevents* you from either interacting with the UI or directly manipulating your application via calls into the model from the `GivenDriver`, both things should be avoided. In the first case, the `GivenDriver`'s purpose is to describe state that exists *before* the user interaction that is being tested. Although this state may be the result of a previous user interaction, your tests will generally be much, much faster if you are able to create this state via API calls rather than driving a web browser. In the second case, by avoiding manipulating your applications's state at the code level and instead doing so via an external API, it is much less likely that you will be creating a state that your application can't actually get into in a production environment. Additionally, this opens up the possibility of running your tests against a "remote" server where you would not have access to the application internals. ("Remote" in the sense that it is not in the same Ruby process as your running tests, although it may or may not be on the same machine.) #### UI Driver #### `Kookaburra::UIDriver` provides the necessary tools for driving your application's user interface using the Window Driver pattern. You will subclass `Kookaburra::UIDriver` for your application and implement your testing DSL within your subclass: # lib/my_application/kookaburra/ui_driver.rb class MyApplication::Kookaburra::UIDriver < Kookaburra::UIDriver # makes an instance of MyApplication::Kookaburra::UIDriver::SignInScreen # available via the private instance method #sign_in_screen ui_component :sign_in_screen def sign_in(account_nickname) account = test_data.fetch_accounts(account_nickname) navigate_to :sign_in_screen sign_in_screen.submit_login(account[:username], account[:password]) end end The call to `Kookaburra::UIDriver.ui_component` defines the UIComponent accessor as a private method in order to discourage accessing your UIComponent objects directly in the test implementation layer. Instead, you should build out your testing DSL in the `UIDriver` subclass as was done with the `#sign_in` method above. ### The Window Driver Layer ### While your `GivenDriver` and `UIDriver` provide a DSL that represents actions your users can perform in your application, the [Window Driver] [Window Driver] layer describes the individual user interface components that the user interacts with to perform these tasks. By describing each interface component using an OOP approach, it is much easier to maintain your acceptance/integration tests, because the implementation details of each component are captured in a single place. If/when that implementation changes, you can---for example---fix every single test that needs to log a user into the system just by updating the SignInScreen class. You describe the various user interface components by sub-classing `Kookaburra::UIDriver::UIComponent`: # lib/my_application/ui_driver/sign_in_screen.rb class MyApplication::Kookaburra::UIDriver::SignInScreen < Kookaburra::UIDriver::UIComponent component_locator '#new_user_session' component_path '/session/new' def username in_component { browser.find('#session_username').value } end def username=(new_value) fill_in('#session_username', :with => new_value) end def password in_component { browser.find('#session_password').value } end def password=(new_value) fill_in('#session_password', :with => new_value) end def submit! click_on('Sign In') no_500_error! end def submit_login(username, password) self.username = username self.password = password submit! end end A `UIComponent` subclass can also contain nested components of its own. For instance: class MyApplication::Kookaburra::UIDriver::UserList::NewUserForm < Kookaburra::UIDriver::UIComponent component_locator '#new_user_form' end class MyApplication::Kookaburra::UIDriver::UserList < Kookaburra::UIDriver::UIComponent component_locator '#user_list' ui_component :new_user_form end In this case, `UserList#new_user_form` is still defined as a private method. In order to manipulate it from your domain driver, you can define either explicit methods or delegators on `UserList`: class MyApplication::Kookaburra::UIDriver::UserList < Kookaburra::UIDriver::UIComponent def fill_in_new_user_form(username, password, full_name) new_user_form.username = username new_user_form.password = password new_user_form.full_name = full_name end delegate :submit!, :to => :new_user_form, :prefix => true end class MyApplication::Kookaburra::UIDriver < Kookaburra::UIDriver ui_component :user_list def create_new_user_with_valid_data user = default_user_data # factory method defined elsewhere user_list.fill_in_new_user_form(user[:username], user[:password], user[:full_name]) user_list.new_user_form_submit! end end ### The Application Driver Layer ### `Kookaburra::APIDriver`, `Kookaburra::UIDriver` and `Kookaburra::UIDriver::UIComponent` rely on the Application Driver layer to interact with your application. In the case of the `APIDriver`, Kookaburra uses `Rack::Test` to send HTTP requests to your application. The `UIDriver` and `UIComponent` rely on whatever is configured as `Kookaburra.adapter`. Presently, we have only used Capybara as the application driver for Kookaburra: Kookaburra.adapter = Capybara It's possible that something other than Capybara could be passed in, as long as that something presented the same API. In reality, using something other than Capybara is likely to require some changes to Kookaburra itself. If you have a particular interest in making this work, please feel free to fork the project and send us a [GitHub pull request] [Pull Request] with your changes. ## Contributing to kookaburra ## * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it * Fork the project * Start a feature/bugfix branch * Commit and push until you are happy with your contribution * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. * Send us a [pull request] [Pull Request] ## Copyright ## Copyright © 2011 Renewable Funding, LLC. See LICENSE.txt for further details. [Window Driver]: http://martinfowler.com/eaaDev/WindowDriver.html "Window Driver - Martin Fowler" [Kookaburra Gem]: https://rubygems.org/gems/kookaburra "kookaburra | RubyGems.org | your community gem host" [Rack]: http://rack.rubyforge.org/ "Rack: a Ruby Webserver Interface" [Capybara]: https://github.com/jnicklas/capybara "jnicklas/capybara - GitHub" [RSpec]: http://rspec.info "RSpec.info: home" [Cucumber]: http://cukes.info/ "Cucumber - Making BDD fun" [Pull Request]: https://github.com/projectdx/kookaburra/pull/new/master "Send a pull request - GitHub"