Bluring the lines


Each state above could be broken out into a separate state machine, so that you could have transition points to perform on a given page, where the sub-state machine would assume that all events, causing a transaction, would not lead to leaving the current page the user is on. This would be helpful for such things as when the user is on a product page, the product page’s state machine would then allow for adding the product to the cart but not navigating to the cart.

Taking a step back - Through the eyes of a Manual Tester

To ensure that we have meaningful QA Automation in place we need to constantly ask ourselves, how does a manual QA tester test a piece of functionality. First and foremost, the tester would document steps for testing a piece of the application. Within the steps they may have a data matrix where one iterates over so that a vast amount of different types of data are used during the steps. For example, if one is testing a cart page, they might have a test case as follows:

# Action Expected Results Notes
1 Navigate to the home page Home Page is displayed
2 Enter a search term, that will return at least one product in the search field and click on the Perform Search button A list of search results should be displayed..more detail for what a search result should contain Use the following SQL to find available products for a term to use:select PRODUCTS.PRODUCT_NAME, PRODUCTS.QUANTITY from PRODUCTS where PRODUCTS.QUANTITY > 1 limit 10;
3 From the search results page click on a random product link The user is directed to a Product Page for the given product
4 On the Product Page, enter a quantity between 1 and the max number available from the SQL noted in step 1 then click the Add to Cart button The shopping cart summary section in the menu bar should indicate a proper count for the number of products in the cart.  There should be a visible element to inform the user that a product was recently added.
5 Click on the Cart button The user is directed to the Cart Page.  Verify that the product added during step 4 is listed and the quantity used during step 4 is accurate.
6 Click on the + button for the product added during step 4. Verify that the quantity has been increased by 1.  Also verify that the DB has an indication that the product’s reserved quantity has been updated.  The reserved quantity should be reset if the user does not complete checkout within 15 minutes so that other users can add the product to their cart.
7 Click on the - button for the product added during step 4. Verify that the quantity has been decreased by 1
8 Click on the - button exactly X times, where X is the quantity of the product in the cart. Verify that the - button has been disabled. Testing to ensure that the - is not available when the quantity is 1.  The user must click the X button to remove the product from the cart.
X A ton more steps to throughly test all items on the cart page, including deleting the product from the cart

Although you have a concrete set of steps to follow a good tester usually performs some ad-hoc testing as well, because you can not reasonably document every possible scenario and combinations of actions for testing a pice of functionality. When a tester is performing ad-hoc testing they would do things such as starting at a product page from a previously copied URL instead of the home page, then execute steps 4-X. As you can see there are many other combinations that somebody might perform before they would want to execute steps 4-X. They might want to start from a checkout page go to home page back to checkout page then to a product page. As you can see the number of possibilities is too much to document in the given time one has to properly test the functionality.

Taking a step Forwards - Devs hat on

Once you have created a thorough state machine representing the application we can easily automatically create many of the ad-hoc type of testing a Manual QA Tester would perform yielding in the optimal level of automation and test coverage. For the rest of this talk, I’ll be using Ruby and the state_machine gem. If you are not a Ruby fan, I’m sure you can find a state machine library for the programming language you are most comfortable using. Defining the state machine:


  require 'state_machine'

  class SomeSiteNavigationASM
     attr_accessor :assertions, :errors, :log
  
      state_machine :state, :initial => :home_page do
          event :navigate_to_home_page do
              transition [
              :product_page, 
              :cart_page, 
              :checkout_page, 
              :checkout_billing_page, 
              :checkout_shipping_page
            ] => :home_page
          end
  
          event :navigate_to_search_results_page do
              transition [
                  :home_page
              ] => :search_results_page
          end
      
          event :navigate_to_product_page do
              transition [
                  :search_results_page, 
                  :cart_page
              ] => :product_page
          end
      
          event :navigate_to_cart_page do
              transition [
                  :home_page, 
                  :search_results_page, 
                  :product_page, 
                  :checkout_page, 
                  :checkout_billing_page, 
                  :checkout_shipping_page
              ] => :cart_page
          end
      
          event :navigate_to_checkout_page do
              transition [
                  :home_page, 
                  :search_results_page, 
                  :product_page, 
                  :cart_page, 
                  :checkout_billing_page, 
                  :checkout_shipping_page
              ] => :checkout_page
          end
      
          event :navigate_to_checkout_billing_page do
              transition [
                  :checkout_page
              ] => :checkout_billing_page
          end
      
          event :navigate_to_checkout_shipping_page do
              transition [
                  :checkout_billing_page
              ] => :checkout_shipping_page
          end
      end
  end

In the provided state machine, I intentionally excluded the option for transitioning from a current page to itself. Introducing loopback transitions would result in a staggering increase in the number of combinations for navigating from one state to another, rendering it impractical in terms of execution time. All of the paths between two states can be obtained by a feature of the state_machine gem:


      asm = SomeSiteNavigationASM.new
      path_collection_home_to_product = StateMachine::PathCollection.new(
          asm, 
          SomeSiteNavigationASM.state_machine, 
          {:from => :home_page, :to => :product_page}
      )

This will return an array of arrays where each inner array is a set of steps on how to get from the home page to a product_page:
  • home_page -> search_results_page -> product_page
  • home_page -> search_results_page -> cart_page -> home_page -> cart_page -> product_page
Plus 831 additional paths for the state machine as defined in this example.

Structure for an Automated test using PathCollection

Once you have a well defined state machine you can see how it’s easy to take advantage of tools out there to help you automate much of the type of ad-hoc testing a manual tester would perform. For the rest of the discussion, I’m going to assume that you already have a well defined automation framework in place for the application under test that the state machine represents and contains means for performing actions such as:


    some_framework.home_page 
    some_framework.add_product_to_cart(:sku => 123) 
    etc..

Using your automation framework you would then update the state machine to include before_transition hooks navigating the user to the appropriate page when entering the state:


    before_transition any => :home_page do 
      automation_framework.home_page 
    end 
    
    before_transition any => :product_page do 
      # our framework allows navigating to a random product if 
      # a specific sku isn’t provided 
      automation_framework.product_page(:random > true) 
    end 

When writing your automated test case it would be a matter of iterating over each inner array within path_collection_home_to_product and performing actions to transition to your next state:


    path_collection_home_to_product.each  |path_set| 
        # on multiple itterations need to reset to the default state
        asm.send("navigate_to_#path_set.first.attributes[:from]") 
        
        # execute 1..N actions for navigating between the home page and product page. 
        path_set.each do |step| 
            asm.send("navigate_to_#{step.attributes[:to]}")
        end
        
        # Now we can simply call our automation that performs the same 
        # steps as the documented manual test case(4-X). 
        automation_framework.product_page_elements.add_to_cart_button.click 
        automation_framework.header_actions.verify_cart_summary 
        automation_framework.header_elements.cart_button.click 
        
        # rest of the method calls to perform testing on the cart page goes here
The above snip now allows us to have 833 test cases, where each test case ensures proper functionality of the cart page, indeterminent on how the user lands on the page.

Conclusion

While working in QA roles, it's common to experience a sense of urgency and time pressure. However, by pausing and adopting well-established concepts, we can achieve thorough test coverage. In the realm of QA, there are various models that can be explored beyond the conventional ones. When faced with the task of creating numerous automated test cases that involve repetitive actions, it's beneficial to question the need for duplicating similar steps across different cases. Instead, consider collaborating with the team to find more efficient and effective solutions to address the underlying problems.