Following the pleasant surprise of learning to build load tests in Visual Studio, my next VS2008 project was tackling a Frankenstein's monster of a web app based on the dreaded Oracle Application Server.
This project took me deep into the workings of VS2008 coded webtests so quickly that I had to document it as I went along and here are my notes preserved for the benefit of all mankind.
Some say this is not testing, it is straight development. I say, it's time for testers to get onboard and drink their own dogfood from the firehose. Testing is changing.
As well as the default tree view, you can also right-click webtests and open them with the XML editor. The structure is as it appears in the tree, but of course it is now easy to perform basic text editing tasks to update the tests en masse.
Once you understand the WebTest and WebTestRequest objects, creating coded webtests is effectively the same as producing a console app.
Armed with these 2 simple pieces of information, this is how I tackled that troublesome OAS app.
Having chosen to use Visual Studio 2008 (VS2008) for automated testing, a number of challenges remain:
Tackling the last problem first:
Each time you log into the AUT, one of the requests is for ww_pa_logon.start_session which effectively creates a new value for PRIMESESSNO and SESSION_ID (among others), which is then passed as a Query String Parameter in any following parameterised request.
When AUT webtests are recorded in VS2008, the current PRIMESESSNO and SESSION_ID values are saved:
When the webtest is played back, the test fails because that session no longer exists. To get around this, we use the Extraction Rule feature of VS2008 to parse the current PRIMESESSNO value from ww_pa_logon.start_session during the log in webtest. In the webtest's XML, we now see:
<ExtractionRules>
<ExtractionRule Classname="...">
<RuleParameters>
<RuleParameter Name="StartsWith" Value="PRIMESESSNO=" />
<RuleParameter Name="EndsWith" Value="&" />
</RuleParameters>
</ExtractionRule>
</ExtractionRules>
The result is that we now have a Context Parameter {{PrimeSessNo}} that any other webtest can refer to. New webtests can be recorded to test any functionality and as long as they include a call to the log in webtest, they will be able to reference {{PrimeSessNo}}. Rather than update this manually for every request in each webtest, this can be performed as a Replace in Files action:
These 2 steps need to be repeated for the SESSION_ID parameter so that every webtest has a valid session.
When a webtest is recorded in the AUT, many of the requests are for empty pages - they are used for layout purposes (don't ask) and are of no interest in functional testing. Leaving them in would clutter up the tests and impact test run speeds. As above, a Replace action can trim the empty requests, e.g. Replace In Files to edit all webtests or for an individual test open as XML, then:
Or you could just delete them from the tree view but any way you do it the empty page requests are removed from the webtest. The static pages /doc/header.htm and /doc/bottom.htm can similarly be disposed of.
Finally, redundant requests for static images can also be ignored at run time by replacing ParseDependentRequests="True" with ParseDependentRequests="False".
It should be noted that the original raw recording should be used for load testing as the trimmed version will make unrealistically light demands on the server and network. For instance, removing these redundant requests sped this particular test up by a third.
(Here's where it gets heavy...)
When the recorded webtests are converted to coded webtests, there is a lot of repetition caused by the number of query string parameters that are repeated between requests. For instance, a webtest that records the process of viewing personal details results in 6 page requests (even after cleaning up as described above). Each request uses an identical base set of query string parameters, so these can be moved into a separate method to clean up the script, allowing the testers to see the essential details of each request and making maintenance that much easier. You end up with something like:
public WebTestRequest parameterisedRequest(string URL)
{
WebTestRequest request = new WebTestRequest(URL);
request.ParseDependentRequests = false;
request.Encoding = System.Text.Encoding.GetEncoding("us-ascii");
request.QueryStringParameters.Add("p_param", "LOGON_ENVNO%3d61", false, false);
request.QueryStringParameters.Add("p_param", "LOGOUT_MENU%3dY", false, false);
request.QueryStringParameters.Add("p_param", "SD01X%3d0001", false, false);
request.QueryStringParameters.Add("p_param",
("SESSION_ID=" + this.Context["SessionID"].ToString()), false, false);
request.QueryStringParameters.Add("p_param",
("PRIMESESSNO=" + this.Context["PrimeSessNo"].ToString()), false, false);
request.QueryStringParameters.Add("p_param", "LANG%3d|", false, false);
request.QueryStringParameters.Add("p_param", "LEVEL%3d1", false, false);
request.QueryStringParameters.Add("p_param", "PROCESSING_NO%3d31109", false, false);
request.QueryStringParameters.Add("p_param", "BGROUP%3dH01", false, false);
request.QueryStringParameters.Add("p_param", "REFNO%3d0004802", false, false);
request.QueryStringParameters.Add("p_param", "FORM_ID%3dAMRBD1", false, false);
request.QueryStringParameters.Add("p_param", "MENU_OPTION%3dY", false, false);
request.QueryStringParameters.Add("p_param", "APPTYPE%3dS", false, false);
request.QueryStringParameters.Add("p_param", "ORGMPCXL_SEQNO%3d557", false, false);
request.QueryStringParameters.Add("p_param", "ORGMPCXH_SEQNO%3d", false, false);
request.QueryStringParameters.Add("p_param",
"FORM_HEADING%3d+Personal+Details+", false, false);
request.QueryStringParameters.Add("p_param", "MENU_ID%3dINET1", false, false);
request.QueryStringParameters.Add("p_param", "SHOWONLY%3dY", false, false);
request.QueryStringParameters.Add("p_param", "LAUNCHEDFROM%3dM", false, false);
return request;
}
Subsequent requests can then be reduced to:
WebTestRequest request3 = parameterisedRequest("http://.../ww_pa_generic.dummy_menucell1");
yield return request3;
request3 = null;
To save having to clutter up every coded webtest with such a large block of code, this idea can be taken further to make parameterisedRequest available to all webtests by creating a Class Library to add to the test project:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.WebTesting;
using System.ComponentModel;
namespace AUTpr
{
[DisplayName("Parameterised Request")]
[Description("Add common QSPs to a request")]
public class AUTRequest
{
public WebTestRequest parameterisedRequest(WebTest thisTest, string URL)
{
WebTestRequest request = new WebTestRequest(URL);
// … As above, only change is to allow the WebTest parameter to be passed in
// so we can use the Context Variables …
request.QueryStringParameters.Add("p_param", ("SESSION_ID=" + thisTest.Context["SessionID"].ToString()), false, false);
request.QueryStringParameters.Add("p_param", ("PRIMESESSNO=" + thisTest.Context["PrimeSessNo"].ToString()), false, false);
// …
return request;
}
}
}
By adding a reference to the AUTpr namespace to the main project, we can now call parameterisedRequest from any coded webtest, by making a few alterations:
using AUTpr;
//...
AUTRequest thisRequest = new AUTRequest();
//...
WebTestRequest request3 = thisRequest.parameterisedRequest(this,
"http://.../ww_pa_generic.dummy_menucell1");
yield return request3;
request3 = null;
The result of all this work is that what was originally a 225 line coded webtest is reduced to a much more manageable (and readable) 80 lines.
OK so that's all very interesting from an intellectual perspective but you still end up with long, confusing scripts that bear no relation to the business logic behind the system, are almost impossible to parameterise and are now so horribly compromised that they are no use even for load testing.
So what now? Well, until coded UI tests arrive in VS2010, there's always WebAii.