With the preliminary work out of the way it is now time to really start getting some effort gains from our service tests. If you’ve just joined us read these relevant posts:
- Part 1 – one small step for writing less code
- Part 2 – focus on the delta
- Part 3 – configuring services for use
The next component I am going to bring into a test script is something I call the pipeline.
The Service Test Pipeline is how we take tests that would be difficult to maintain, read or write and make them more resistant to change, read more like a structured English test script and simple to write.
If you’re familiar with writing service tests; you are going to have a lot of code that looks like this:
web_service_call
(
"StepName={Service}::{Method}",
"SOAPMethod={Service}|{Binding}|{Method}",
"ResponseParam=response",
"Service={Service}",
"ExpectedResponse=SoapResult",
BEGIN_ARGUMENTS,
END_ARGUMENTS,
BEGIN_RESULT,
END_RESULT,
LAST
);
//Validate results
lr_xml_extract
(
"XML={response}",
"FastQuery=Envelope/Body/{Response}/{Result}/path/to/node",
"XMLFragmentParam=XmlBlob",
LAST
);
success = lr_xml_find
(
"XML={response}",
"Query=/Envelope/Body/{Response}/{Result}/path/to/node",
"Value=VerifiedAgainstMe",
"NotFound=Continue",
LAST
);
if (!success)
{
lr_error_message (“all aboard the failboat!”) ;
}
What we are going for is something that looks like this:
SetDefaultParameters () ;
CallService () ;
...
ExtractResult () ;
VerifyStringsAreEqual (“Result_Path_To_Node”, “VerifiedAgainstMe”) ;
The three main functions we have are the three difference pipelines:
- Default parameters
- Call service
- Extract result
Big difference.
How it Works
When you make a call to one of the three methods: SetDefaultParameters, CallService or ExtractResult the underlying method will examine the Service property and will call a subsequent router for that service. Here is an example of the CallService router:
void CallService (void)
{
if (StringsAreEqual (lr_get ("{Service}"), "MyAwesomeService"))
{
MyAwesomeService () ;
}
}
The call to MyAwesomeService then examines the Method property to determine which service operation to call. Here is an example:
void MyAwesomeService (void)
{
if (StringsAreEqual (lr_get ("{Method}"), "IsAwesome"))
{
MyAwesomeService_IsAwesome () ;
}
}
That call then makes the web service call; much like the one you would have in your scripts previously.
void MyAwesomeService_IsAwesome (void)
{
web_service_call
(
"StepName={Service}::{Method}",
"SOAPMethod={Service}|{Binding}|{Method}",
"ResponseParam=response",
"Service={Service}",
"ExpectedResponse=AnySoap",
BEGIN_ARGUMENTS,
"whoIsAwesome={whoIsAwesome}”,
END_ARGUMENTS,
BEGIN_RESULT,
END_RESULT,
LAST
);
}
The other two routers work in the same way, the name of their underlying methods are different but the process is the same. It is important that the Service and Method are correctly configured as per the script initialisation process.
SetDefaultParameters
You will notice that the web service call pipeline doesn’t accept any parameters. The arguments for the service call are all set to HP Property names that are match the spelling of the service property names. The establishment of these properties is handled by the SetDefaultParameters pipeline. The code for that looks like this:
void MyAwesomeService_SetDefaultParameters_IsAwesome (void)
{
lr_set ("distributedlife", "whoIsAwesome");
}
Each property is set to its default value. The goal is to have a web service call that works just using the defaults only. The default parameters should also be the bare minimum of values necessary to make the service request succeed. If the defaults values need to be changed then in your script make a call to SetParameterValue like so:
SetParameterValue ("whoIsAwesome", "SomeBodyElse") ;
This will override the default. The next call to SetDefaultParameters, included in every test, will result in the slate being clean for the next test.
ExtractResult
The extract result pipeline is designed to make it easier to validate the results of the service after the test has been run. It works like the other pipeline where a method is written for each operation to specifically extract the response object and put it into a standardised variable for use within the test.
When you are writing your own extract result; there are only three types that need to be catered for: single result returned, one object returned and a collection returned. This are discussed below.
The best way to determine what is a single value, an object or an array; take a look at the Add Service Call button in Service Test. It will indicate what is an object and what is an array of objects.
Single value returned
The easiest to handle and the same code should be used for all tests like this. The value is stored in a parameter called ResultValue.
void MyAwesomeService_ExtractResult_IsAwesome (void)
{
//always make sure we reset our values before use
lr_set ("", "ResultValue") ;
lr_xml_get_values
(
"XML={response}",
"ValueParam=ResultValue",
"Query=/Envelope/Body/{Response}/{Result}",
LAST
) ;
}
If you want to use this value in your tests then simply make use of:
lr_get ("{ResultValue}") ;
Single object returned
The next most simple scenario is when an object is returned. This object may have child objects but in all scenarios only one of each is returned.
The format for parameters is Result_FieldName where the field name is the name of field on the response object.
Examples:
- Result_Name
- Result_Address
- Result_MobileNumber
If your response object has a child object then simply add the ObjectName into the result variable name.
Examples (if the AwesomePerson was a separate object):
- Result_AwesomePerson_Id
- Result_AwesomePerson_Number
Here is how the code should be written in your ExtractResult implementation for operations that return a single object.
void MyAwesomeService_ExtractResult_GetAwesomePerson (void)
{
lr_set ("", "Result_Name") ;
lr_set ("", "Result_Address") ;
lr_set ("", "Result_MobileNumber") ;
lr_xml_get_values
(
"XML={response}",
"ValueParam=Result_Name",
"Query=/Envelope/Body/{Response}/{Result}/Name",
LAST
) ;
lr_xml_get_values
(
"XML={response}",
"ValueParam=Result_Address",
"Query=/Envelope/Body/{Response}/{Result}/Address",
LAST
) ;
lr_xml_get_values
(
"XML={response}",
"ValueParam=Result_MobileNumber",
"Query=/Envelope/Body/{Response}/{Result}/MobileNumber",
LAST
) ;
}
As you can see it is a collection XPATHs that get each value out and stores it in the named parameter. Nothing complicated here.
Collection returned
Collections are more complicated. A collection of collections is harder again. The process is the same for them all but each one requires a little bit more setup to ensure that all values are being extracted.
Be aware that this kind of processing does increase script execution time and should not be used for performance related tests.
I’ll start with a code sample before discussing how it works:
void MyAwesomeService_ExtractResult_GetAwesomePeople(void)
{
//the result is an array (the AwesomePeople node is arrayed)
//Parameters are stored using the name: Result_AwesomePeople_i_FieldName
// i is the index number in the array
// FieldName is the name of the property attached to the returned object
unsigned int ResultCount = 0 ;
unsigned int i = 0 ;
ResultCount = lr_xml_extract
(
"XML={response}",
"Query=/Envelope/Body/{Response}/{Result}/AwesomePeople",
"XMLFragmentParam=__Ignore", "NotFound=Continue", "SelectAll=Yes",
LAST
) ;
//save the result count for use in the test
lr_save_int (ResultCount, "ResultCount") ;
//don't extract results if we don't have any
if (ResultCount == 0)
{
return ;
}
for (i = 1; i <= ResultCount; i++)
{
//build parameter name prefix
lr_save_int (i, "__i") ;
lr_set ("", lr_get ("Result_AwesomePeople_{__i}_Name")) ;
lr_set ("", lr_get ("Result_AwesomePeople_{__i}_Address")) ;
lr_set ("", lr_get ("Result_AwesomePeople_{__i}_MobileNumber")) ;
lr_xml_get_values
(
"XML={response}",
lr_get ("ValueParam=Result_AwesomePeople_{__i}_Name"),
lr_get ("Query=/Envelope/Body/{Response}/{Result}/AwesomePeople[{__i}]/Name"),
LAST
) ;
lr_xml_get_values
(
"XML={response}",
lr_get ("ValueParam=Result_AwesomePeople_{__i}_Address"),
lr_get ("Query=/Envelope/Body/{Response}/{Result}/AwesomePeople[{__i}]/Address"),
LAST
) ;
lr_xml_get_values
(
"XML={response}",
lr_get ("ValueParam=Result_AwesomePeople_{__i}_MobileNumber"),
lr_get ("Query=/Envelope/Body/{Response}/{Result}/AwesomePeople[{__i}]/MobileNumber"),
LAST
) ;
}
}
The first step is to count the number of elements in the array. This should be done for each array in your collection. Use a separate variable for each one. If the total number of elements in the main collection is zero; do not both continuing.
For each element you will need to extract the object values and store them in a well named parameter. The process is
Result_ + ObjectName_ + i_ + FieldName
Where:
- ObjectName is the name of the collection
- i is the element number; remembering that XPATH results start from one
- FieldName is the name of the field.
If you have child collections then you will have nested loops and your format will look more like this:
Result_ + ObjectName_ + i_ ChildObjectName_ + j_ + FieldName
Where additional object names and element numbers are added into the name until all are documented.
Remember that your tests should be scoped so that thousands of results do not get returned. This will reduce the number of variables created and reduce the processing time.
Once again; to use any of these values then simple call:
lr_get ("{Result_AwesomePeople_1_Name}") ;
or similar.
Conclusion
This is a big change in how you write tests. Let me reiterate what tests should look like now:
//Fixtures
SetDefaultParameters () ;
SetParameterValue ("WhoIsAwesome", "Distributedlife") ;
CallService () ;
...
ExtractResult () ;
//Result Validation
Code like this is easy to write and reads like a test. When I get to fixtures and result validation it is going to get easier still. When you have a service contract change you only have to update your scripts in one place. Not every test. We have tens of thousands of service tests1 and that number grows substantially every day.
Are we worried about change? Not in the slightest. A service contract change will result in additional properties; some optional, some mandatory. We update the necessary pipeline code, delete and update tests as required and move on. Tests that are not impacted do not change unlike the standard way of writing HP Service Tests.
This method writing service test also reduces the barriers to new service testers. If you have a existing tester and you give them a list of functions they can use they are able to pieces together scripts with only basic knowledge of Vanilla C.
The only cost of the pipeline comes when a new service is delivered and when a service contract changes. The rest of the time it is smooth sailing and easy testing.
- I don’t know how many as I’m still writing a tool to count them all [↩]
|
|
Ryan Boucher is a Software Inquisitor and is passionate about it. You can find a whole raft of articles and anecdotes about software testing and other topics he gets excited about. |
| Tags |