Designing maintainable calabash tests using Screen Objects: Part 2

Background:

In a previous example entitled “Designing maintainable calabash tests using Screen Objects” I introduced a possible solution to creating maintainable cross platform calabash tests. Whilst this was a good start, there was room for improvement.

The Problem:

While on the face of it the first example may seem fine, but it could have implications. The fact that we are accessing calabash commands directly from our screen-objects, and the inability to drive the tests using different drivers (should you decide) could provide a maintenance headache in the future.

The Solution:

We refactor our Base Class, abstracting all common driver (calabash) actions and write our own wrapper methods around these. We can then leverage them in our screen-objects.

An Example:

In this example we are going to refactor the code from the “previous example.


require 'calabash-android'
require 'calabash-android/operations'

class BaseClass
  include Calabash::Android::Operations

  def initialize(driver)
    @driver = driver
  end

  def method_missing(sym, *args, &block)
    @driver.send sym, *args, &block
  end

  def tap_on(element)
    touch("* marked:'#{element}'")
  end

  def exists?(element)
    element_exists("* marked:'#{element}'")
  end

  def keyboard_enter_text(el, text)
    query("* id:'#{el}'", {:setText => text})
  end

  def wait_for_no_progress_bars
    performAction('wait_for_no_progress_bars')
  end

  def wait_for_dialog_to_close
    performAction('wait_for_dialog_to_close')
  end

  def wait_for_element(element)
    wait_for { exists?(element) }
  end

  def self.element(name, &block)
    define_method(name.to_s, &block)
  end

  class << self
    alias :value :element
    alias :action :element
  end

end

As you can see above, within our base-class we now have a set of common methods that wrap around calabash commands. I have also removed reference to Calabash ABase. The previous example inherited from the Calabash base class. There’s no need for this now we are creating our own wrapper methods around calabash commands.

Now we have refactored this, (and like the previous examples) the screen-objects will inherit from it. We must now refactor our sceen-objects to remove any direct calls to calabash commands, and replace them with our custom methods.

Before:


class LoginScreen < DroidPress

element(:username_field)     { 'username' }
element(:password_field)     { 'password' }
element(:login_button)       { 'save' }

value(:not_logged_in?)       { element_exists("* id:'#{login_button}'") }

action(:touch_login_button)  { touch("* id:'#{login_button}'") }

def login_with(username, password)
 query("* id:'#{username_field}'", {:setText => username})
 query("* id:'#{password_field}'", {:setText => password})
 performAction('scroll_down')
 touch_login_button
 performAction('wait_for_no_progress_bars')
 performAction('wait_for_dialog_to_close')
end

end

After:


class LoginScreen < BaseClass

  element(:username_field)     { 'nux_username' }
  element(:password_field)     { 'nux_password' }
  element(:login_button)       { 'nux_sign_in_button' }
  element(:forgot_password)    { 'forgot_password' }
  element(:create_account_btn) { 'nux_create_account_button' }

  value(:await)                { wait_for_element(username_field) }
  value(:not_logged_in?)       { exists?(username_field) }

  action(:touch_login_button)  { tap_on(username_field) }

  def login_with(username, password)
    keyboard_enter_text(username_field, username)
    keyboard_enter_text(password_field, password)
    touch_login_button
    wait_for_no_progress_bars
  end

end

As you can see little has changed, the only thing we have done is called our wrapper methods from the base class instead of calling calabash commands directly (The ID’s etc have changed slightly as this example is using the latest version of the wordpress app).

Once this is complete our step definitions remain unchanged, however to remove duplication I decided to refactor these slightly:

Before:


Given(/^the app is launched$/) do

  @screen = page(WordPressApp)

end

When(/^I login with (valid|invalid) credentials to Add WordPress.com blog$/) do |negate|

  @screen.welcome_screen.await
  @screen.welcome_screen.touch_add_blog

  @screen.login_screen.await
  @screen.login_screen.login_with(USERS[:valid][:email], USERS[:valid][:password]) if negate.eql? 'valid'
  @screen.login_screen.login_with(USERS[:invalid][:email], USERS[:invalid][:password]) if negate.eql? 'invalid'

end

Then /^I (should|should not) be logged in$/ do |negate|

  if negate.include? 'not'
    @screen.login_screen.should be_not_logged_in
  else
    @screen.home_screen.await
    @screen.home_screen.should be_logged_in
  end

end

After:


When(/^I login with (valid|invalid) credentials to Add WordPress.com blog$/) do |negate|

  @screen.login_screen.await
  @screen.login_screen.login_with(USERS[:valid][:email], USERS[:valid][:password]) if negate.eql? 'valid'
  @screen.login_screen.login_with(USERS[:invalid][:email], USERS[:invalid][:password]) if negate.eql? 'invalid'

end

Then /^I (should|should not) be logged in$/ do |negate|

  if negate.include? 'not'
    @screen.login_screen.should be_not_logged_in
  else
    @screen.home_screen.should be_logged_in
  end

end

You will see that I have removed the ‘Given the app is launched’ step. Rather than initialize this class at the beginning of each scenario I moved into a hook:


#hooks.rb file (lives in the support dir)

Before do |scenario|
  @screen = WordPressApp.new(self)
end

This hook will initialize our screen-objects before each scenario is executed.

Summary

We are now in a position where our screen-objects can access common actions without directly accessing calabash code. Therefore, if in the event that calabash commands change, you will only have to refactor one class.

As well as this, should you decide to switch drivers (for example Appium) it would require little effort do do this.

All example code can be found on GitHub, here

Thanks for reading, any feedback welcome!

– Ian

Advertisements

12 Comments

  1. Hi, 2 great posts!

    A couple of newbie questions if I may:

    1) I have followed your posts to create a small framework for an app I am working on. However, when I run calabash I am getting a warning around the define_method call – “tried to create Proc object without a block”. Is this expected? If not, can you advise how to resolve?

    2) I need to be able to store contents from a screen in the “When” step and check it in the “Then” step – can you advise on the best way to do this?

    Thanks.

  2. Hi Steve,

    Point 1:
    You can fix that warning by something like:
    def method_missing sym, *args, &block
    send sym, *args, &block
    end

    Point 2:
    Depending on preference, you can use an instance variable, like:

    When I want to store text
    @text = query(“* marked:’your_id'”)

    Then I should see my text:
    element_exists(“* marked:’#{@text}'”)

    However the most maintainable way to do it would be to return it from a method within your screen classes:

    For example:

    When(/^I create a new blog post$/) do

    @screen.home_screen.touch_newpost_btn
    @screen.new_post_screen.await
    @blog_title, @blog_content = @screen.new_post_screen.add_new_blog_post

    end

    The method that returns the text (from my a page class):

    def add_new_blog_post
    title = (0…25).map { (‘a’..’z’).to_a[rand(26)] }.join
    content = (0…250).map { (‘a’..’z’).to_a[rand(26)] }.join
    query(“* id:’#{blog_title}'”, {:setText => title})
    query(“* id:’#{post_content}'”, {:setText => content})
    performAction(‘scroll_down’)
    touch_publish
    performAction(‘wait_for_no_progress_bars’)
    return title, content
    end

    Then(/^the post should be added$/) do

    @screen.home_screen.await
    @screen.home_screen.touch_view_posts_btn
    @screen.posts_screen.await
    exists?(@blog_title) && exists?(@blog_content)

    end

    Hope that helps.

    1. Hi Ian,

      Thanks for the above. method_missing is already defined as per your response (which I believe is the same as your example project). Not sure if I am missing some other code or if I am using the wrong send library???

      For the second point, is there anyway to have the instance variables declared inside the relevant screen class? For example, can @blog_title and @blog_content be set directly in the new_post_screen class and then checked in the Then step with something like @screen.new_post_screen.blog_title.should eg (“blog title”).

      Thanks,

      Steve

      1. The first warning, I’ll have to look at my example code when I get to my machine, but in the meantime you could try:
        def method_missing sym, *args, &block
        @world.send sym, *args, &block
        end

        Secondly, not sure I follow. But you may want to look up attribute_accessor, which creates an instance variable that can be used within your screen class, and step definitions.

        class Test
        attr_accessor :blog_title, :blog_content

        value(:blog_content?) { exists?(@blog_title) }

        def add_new_blog_post
        @blog_title = (0…25).map { (‘a’..’z’).to_a[rand(26)] }.join
        @blog_content = (0…250).map { (‘a’..’z’).to_a[rand(26)] }.join
        query(“* id:’#{blog_title}’”, {:setText => title})
        query(“* id:’#{post_content}’”, {:setText => content})
        performAction(‘scroll_down’)
        touch_publish
        performAction(‘wait_for_no_progress_bars’)
        end

  3. Thanks for the above Ian. I will use attr_accessor for what I need.

    For the first point, I still see the error when using:


    def self.element element_name
    define_method element_name.to_s
    end

    The define_method call expects the block to passed to it but we are not - is it because of the way the elements are set in the screen class files?

    Thanks again for all your help.

    Steve

    1. Hi Steve,

      Apologies for not getting back to you, been quite busy.

      Based on your comments I am going to refactor and update the above blog post to remove this warning. Thanks for pointing that out to me, I missed it. Although the tests will still run as normal as they are in the above example, its not good to have these annoying warnings.

      Expect an update later today/tomorrow.

      1. Hi Steve, I’ve now updated the blog. Apologies for missing the error. It was staring me in the face all along!

        Hope that helps you get set up designing maintainable framework.

  4. Hey Ian, maybe I am missing something but how do you call .await once you have abstracted out the ABase?
    @screen.login_screen.await

    1. Hi John, thanks for the comment. If you checkout the github project you will see that I am defining my own method called await within the welcome_screen/login_screen classes.

      Hope that helps, if not let me know.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s