We've looked at the concepts of a Framework, Components and Architecture that are used to describe test automation projects. Another key concept in automated testing is that of abstraction. In this article we look at the concept of abstraction and explain why it has such a pivotal role in the construction of our automation frameworks.
In general terms Abstraction is defined as follows:
the process of formulating generalised ideas or concepts by extracting common qualities from specific examples
In order to demonstrate the concept of abstraction we're going to use a conceptual diagram that is based on a Library based automation framework.
Please note that we're trying to keep away from any code specifics and any specific coding language. We do touch of a few lines of very simple pseudo code just to highlight some of the concepts though.
We're starting out with a Library based framework with the following components and architecture to help us demonstrate this concept:
This framework has a number of linked components that help us maintain and develop our test cases. These components are described in detail below but to just set the context let's describe the high level concepts first.
Moving from left to right we have; our Control script that defines a list of test cases and what those test cases should do. It does this by calling the test "Actions", "Menu Navigation", "Helper Functions" and/or the "Sequences" we need to run. When we execute the control script it will call a method in one of the following Libraries
The 'Actions' script
The 'MenuNavigation' script
The 'Helper Functions' script
The 'Sequences' scripts
These Action, Menu Navigation, Helper and Sequence scripts basically contain bits of code (functions and methods if you want to get technical) that do different things with your application. What we've done here is take all the possible bits that we need to build our scripts with. Then we've Abstracted out the related bits into files/scripts. These gives us these four core areas; Actions, Menu Navigation, Helper Functions and Sequences'. Before we drill down into the detail each of these scripts is responsible for:
Menu Navigation: When you call the Menu Navigation methods they carry out navigation actions directly against the application under test (e.g. navigate to a specific page by clicking on a menu link in header bar).
Helper Functions: These are generic functions that might not be used as part of the tests themselves but need to be available in order to connect the dots. For example you might have functions to start a particular browser, read a data file, etc in here.
Actions: When you call the Actions methods they then call detailed methods in files like the 'Components' file. You can have multiple Components files for different areas of the application under test. Each 'Components*' file contains a list of detailed steps to run against a specific piece of functionality within the application under test. For example you might have an 'enter New User record' action that has a related 'enter New User Record' method in a 'Components' script.
Sequences: When you call a Sequence method it will call a sequence of actions in the 'Actions' file (which then calls the required 'Components* methods). A sequence is a list of actions you need to call in turn. For example you might have a sequence for 'Create New user and Modify User'. This will call actions like Enter New User Record, Modify User, etc. Sequence methods can also call Menu Navigation methods directly.
Utils: In the Utils script we have some low level Utility methods. These might be responsible for entering text in specific types of fields or for making API calls. For example a method in your components script might need to confirm that some other related data is in place before creating a new user record. If this is the case you might have a utility method that makes and API call to confirm this.
Looking at each of those components in a bit more detail we have the following core components...
This is responsible for defining, and calling, all the test steps that make up the test cases. The control script basically lists all the steps that automation project will run. Those steps can included :
i. actions: a discrete action that can be carried out against your application
ii. sequences: a sequence that calls a series of actions in turn
iii. navigation: a navigation task carried out within the application
iv. helper functions: generic functions that complete tasks that aren't really part of the test but are required to run the tests (like starting the application under test)
v. utility functions: low level utilities that help process data or aid in the interaction with the application under test
The control script should ONLY directly call functions within the four script unit relating to actions, sequences, navigation and helper functions. For example:
Actions : contains a list of all the action methods this automation project supports Sequences: contains a list of all the sequence methods that make up sets of actions MenuNavigation: contains all the functions needed to navigate through the application HelperFunctions: contains generic functions used to complete high level actions (like start a browser)
An example set of calls, defined in the control script, used to run a test might be as follows:
HelperFunctions\StartApplication() MenuNavigation\NavigateToHomePage() Actions\EnterLoginDetails() Actions\CheckLoginSuccess() Actions\LogOut() HelperFunctions\CloseApplication()
With this control script we've defined the steps for a test case that tests the login process. You can see from this that we call functions only in HelperFunctions, MenuNavigation and Actions. What we've done is 'abstract' out all of our high level test steps into the control script. All of the heavy lifting is contained in other scripts which group methods/functions into logical areas.
Now we understand how the high level control script works we can look at how these calls drive the interaction with our application under test. It's these lower level scripts and functions that we'll look at next.
This library of functions is responsible for carrying out high level system type actions. So things that are NOT specific to any particular application under test. It's a library of functions that can be shared and used on many different automation projects.
Typical functions that are used within the HelperFunction library are....
helperLaunchIE(): starts Internet Explorer helperCloseIE(): closes Internet Explorer helperNavigatetoPage(): navigates to a web page helperCheckPageLoaded(): checks to see if a web page has fully finished loading
These helper functions can be called from the control script as part of the execution of a sequence items that make up a test case, as follows:
HelperFunctions\helperLaunchIE() HelperFunctions\helperNavigatetoPage() <other application specific calls> HelperFunctions\helperCheckPageLoaded() HelperFunctions\helperCloseIE()
As these functions are generic system level or high level application functions this script can be used across many different automation projects. In fact the same library file would be included in different projects that need the same set of functions (in this kind of setup we might use something like "SVN externals" to ensure all automation projects end up with the same source code for the Helper Functions - but that's a topic for another day).
This library of functions is responsible for carrying out all the navigation actions within the application. If you need to click on a tab, need to select a side menu link or even set application preferences these are the functions to use. None of these functions are responsible for entering any data, it's all navigation and application setup. Navigation and setup so that the application is in the right place and state to start carrying out test actions (which we discuss next).
Typical functions that are contained within the MenuNavigation library are.....
navLeftMenuLinks(link_text): will click/select a link in the left hand navigation panel navTab(tab_text): will click on a tab in the application navMainMenuBar(link_text): will click on a link in the main menu navSubMenuBar(link_text): will click on a link in the sub menu (e.g. used after you've select the main item )
These menu navigation functions can be called from the control script as part of the execution of a sequence items that make up a test case, as follows:
HelperFunctions\helperLaunchIE() HelperFunctions\helperNavigatetoPage() MenuNavigation\navTab("abc") MenuNavigation\navMainMenuBar("Menu Item 1") MenuNavigation\navSubMenuBar("Sub Menu Item a") HelperFunctions\helperCheckPageLoaded() HelperFunctions\helperCloseIE()
These functions are all specific to the application under test and should be used for all navigation within that application. This script can be pulled into other project libraries where navigation tasks are required. Calling these functions in conjunction with other functions in the HelperFunctions library starts to deliver the capability to construct complex automated test scenarios.
This library is responsible for 'listing' all the actions that you can carry out on the application under test. There's an IMPORTANT distinction here. It's only responsible for 'listing' the actions and 'calling' other scripts where the actual actions are defined. We do this so that we can have another layer of abstraction. As our library files that contain actions grow we'll eventually want to split them into separate action library file (e.g. action library file for login/logout related functions, action library file for functions related to creating new records, etc). By implementing this layer of abstraction with one overall action file that lists everything we give ourselves:
1. one file with a list of all the possible actions we can employ which makes it easier for the control script to just call a function from what is essentially a list 2. separate action files that contain related functions which makes the code easier to manage because all related functions are kept in separate files
Anyway, you can think of an action as a discreet, standalone, set of steps that will complete a single unit of work within the application under test. Actions are then functions defined in the separate action library files. And this overall action file is basically a list of all available actions that the user can select from. Calls to action functions in the parent action file then make calls to the detailed functions in the child action files.
For example a typical chain of events could be:
Control Script -calls--> Actions/actionLogin() -calls ---> actionsUserFunctions/Login()
So this parent Action file acts as a pick list of actions that you can choose from when you're building out your test cases in the control script. So for example when you add a 'Test Case' to the control script you just pick from the list of actions like this.......
HelperFunctions\helperLaunchIE() HelperFunctions\helperNavigatetoPage() Actions\actionLogin() MenuNavigation\navTab("abc") MenuNavigation\navMainMenuBar("Menu Item 1") MenuNavigation\navSubMenuBar("Sub Menu Item a") HelperFunctions\helperCheckPageLoaded() Actions\actionLogout() HelperFunctions\helperCloseIE()
The point is that the Actions script acts as a layer of abstraction that helps simplify things in three ways...
i. when you go to select an action from the list in the control script you only have to search in one file for your action. No hunting through multiple files to find what you want
ii. It makes it easier to wrap actions with supporting code. For example you can have an action that you might want to time from one run to the next. So in the parent actions script we'll have....
def actionLogin(): StartTimer() actionsUserFunctions\Login() StopTimer()
With this sort of approach we can time how long it takes to execute certain actions, and it's clearly visible that we're applying this specifically to the actionLogin() function
iii. we can add pre-requisite actions and post completion actions to make sure the application is in the right state before and after an action is completed. For example we might have...
def actionLogin(): HelperFunctions\helperNavigatetoPage() actionsUserFunctions\Login()
In this way if an actions is totally dependent on some other action or navigation then you can ensure this is defined here. This removes some calls from the control script and simplifies the control script. Yet another way of abstracting information out from one file to another file in order to simplify things.
In some circumstances we might find it easier to link repeated sets of actions together to form a sequence of actions. So rather than lots calls to actions from the control script (calling individual 'action' functions) we'll define a sequence function that contains the calls to the individual action functions. Then we can call this sequence from the control script when we need to run the "Sequence of Actions". The sequence will be responsible for calling all the 'actions'.
This can be seen in the example below where we have a set of actions. A set of actions that we might end up repeating a lot of times :
def control_script(): HelperFunctions\helperNavigatetoPage() Actions\actionLogin() MenuNavigation\navTab("abc") MenuNavigation\navMainMenuBar("Menu Item 1") MenuNavigation\navSubMenuBar("Sub Menu Item a") HelperFunctions\helperCheckPageLoaded()
This is a list of actions, menu navigation and helper functions that are called from our control script. What we can do is pull these out of the control script and put them in the sequence script like this...
def seqLoginAndNavigateTo(): HelperFunctions\helperNavigatetoPage() Actions\actionLogin() MenuNavigation\navTab("abc") MenuNavigation\navMainMenuBar("Menu Item 1") MenuNavigation\navSubMenuBar("Sub Menu Item a") HelperFunctions\helperCheckPageLoaded()
Then the control script only needs to contain...
def control_script(): seqLoginAndNavigateTo()
Which, I'm sure you agree makes the control script a lot easier to read and understand. At this point we now have three layers of abstraction implemented.....
Control Script Sequences Script Actions
If we're going to use a series of actions repeatedly then we're better of turning this into a 'sequence' function. A sequence function that that then calls all the required action functions. With this in place we have the option to define our test items as granular steps using 'actions' or one big step using 'sequences'.
When selecting the functions to call from the Control Script only functions from these four libraries should be called...
Actions Sequences MenuNavigation HelperFunctions
In the Control script then when define the functions to call, ONLY ever select from these four categories above. They are the only four library files you should need to select from anyway. Doing it this way keeps it simple to follow. You either need to carry out an action, a sequence of actions, navigation within the application or some helper function. All the detail and complexity should be contained in scripts/libraries underneath these files.
To pull together a simple functional test script you define these calls in the control script:
HelperFunctions\helperStartApp() Sequences\seqLoginAndNavigateTo() Actions\actEnterRecord() MenuNavigation\navListRecords() Actions\actCheckRecordList() Sequences\seqLogOut() HelperFunctions\helperCloseApp()
You can see from this that we can build out a complete test script in just 7 lines of code. These different helper, navigation, sequence and action functions coming together to start your application, login, enter record, check the records has been entered, logout and close the application. All the detail and complexity is held in the scripts below this. Having a short list of calls like this in the control script makes if far easier to read, maintain and update.
In selecting functions from these four script/libraries you have the ability to quickly pull together simple or complex test scenarios as required. Underlying the functions in these script units is the complexity for completing all of these actions. This complexity split into script/library files that are designed to carry out the specifics assoicated with things like actions. It's these specifics we'll look at next.
As we've already learnt, the Actions and Sequence files only contain a list of actions and sequences we can use. These files don't actually contain the scripts or code that carry out the actions. Lower level Function scripts hold all the code that does the actual work.
Let's take the actions file as an example (we'll come on to sequences in a moment). This has a list of all the actions that we can call and use. The detailed script/code for those actions is actually held in the some lower level files 'Function' files. The flow of calls between these files then goes as follows:
Control Script -calls--> Actions/ -calls ---> actionsUserFunctions/userLogin() -calls ---> actionsUserFunctions/userLogout() -calls ---> actionsAccountFunctions/accountsNewAccount() -calls ---> actionsAccountFunctions/accountsModifyAccount()
In our control script we list all the 'actions' we need to call. Each time we call an action in the 'Actions' script, this then calls functions in the lower level actionsUserFunctions and actionsAccountFunctions.
In this way we have our top level action file containing a list of ALL the actions we have. Yet we've neatly split out all our specific actions into functions grouped by application area (in our example User functions and Account functions). This makes everything far easier to maintain. You don't end up with one massive file full of functions that are used to carry out actions. You have one list script (the higher level 'Actions' file) that at run time automatically branches off to find the detailed action function in the required actions file (e.g. actionsAccountFunctions or actionsUserFunctions).
Now lets add sequences into this. Remember that we add sequences when we want to group a load of actions into one function. With this the Control Script will make a call to a sequence function and that sequence function will in turn call of the actions functions that it has listed. This will look something like this...
Control Script -calls--> Sequences -calls--> Actions/ -calls ---> actionsUserFunctions/userLogin() -calls ---> actionsUserFunctions/userLogout() -calls ---> actionsAccountFunctions/accountsNewAccount() -calls ---> actionsAccountFunctions/accountsModifyAccount() In this case then our sscript might do something like this: HelperFunctions\helperStartApp() Sequences\seqLoginAndNavigateTo()
Where the Sequences\seqLoginAndNavigateTo() call contains something like this...
def seqLoginAndNavigateTo(): actionsUserFunctions/userLogin() actionsNavigate/NavigateTo() actionsNavigate/NavigateCheck()
In this example we can abstraction out three steps from the control script (that make direct action calls) and replace them with one sequence call.
The final component in this framework is the Utils script. This script contains, not unsurprisingly, generic utility functions that we want to avoid duplicating in the different 'function' scripts. For example we might have functions for...
randomInt: creates a random integer (for things like unique ids used in some tests)
enterText: perhaps the application has some peculiarities when entering data in text boxes and drop downs. This function might work round these peculiarities and ensures more consistent results with data entry
If you find yourself repeating code in the 'components' files then pull this code out and place it in the Utils file. So we refactor our code to abstract out common functions into reusable functions in one utils file.
In essence we're layering up our automation framework so that everything is in it's logical place. We end up with a hierarchy of scripts that make everything far easier to maintain and understand.
Control Script Sequences -> Actions / Navigation actionsFunctions Utils
So what we finally have then is....
A good framework allows you to keep your code neat, manageable and easy to build on. You can achieve this by focusing on abstraction and making sure the right components are abstracted into the right libraries, files and functions. To enable this we've built constructed an automation framework around 7 types of script giving us effectively 7 layers of abstraction:
Structuring these scripts in a hierarchy enables us to logically organise the areas of abstraction. This in turn gives us the flexibility and ease, to create automated test cases that are built using the principles of re-use and deliver a simple but maintainable code base.