Yesterday, I talked about the basics of creating a new web test. Today, I will talk about how to customize a web test. When you build a standard web test is uses fixed values that would have to be changed manually every time the test was run. For example, you would have to change the pickup date to make sure the dates were valid. If you create a coded web test, this is no problem since you can have the pickup date based off of today's date.
Although you can create a coded web test by inheriting from the WebTest class, the easiest way to create one is by clicking the Generate Code button. Once you click that button a new class will be created that inherits from WebTest and overrides the GetRequestEnumerator() method. It is this method where all the code goes.
In this example, I am going to show how to make a simple test to verify that a rate shop was successful. We'll look at this a block at a time. Remember this code is autogenerated. The first test will verify that the home page loaded successfully by looking for the string GET RATES (next to the go button).
WebTestRequest request1 =
new WebTestRequest("https://tweb46/reservations/index.aspx");
request1.ThinkTime = 5;
ValidationRuleFindText rule1 = new ValidationRuleFindText();
rule1.FindText = "Get Rates";
rule1.IgnoreCase = true;
rule1.PassIfTextFound = true;
request1.ValidateResponse
+= new EventHandler<ValidationEventArgs>(rule1.Validate);
ExtractHiddenFields rule2 = new ExtractHiddenFields();
rule2.ContextParameterName = "1";
request1.ExtractValues
+= new EventHandler<ExtractionEventArgs>(rule2.Extract);
yield return request1;
A new WebTestRequest object is created for every postback that occurs. The ThinkTime parameter is the amount of seconds that the test should wait before proceeding to the next step (this value is set by recording the test). Typically, I change this to 0 or 1, so that the test will execute as fast as possible. The section after that creates a validation rule that looks for the text Get Rates. The section after that creates the code that automatically extracts the viewstate parameters. Lastly yield return request1, sends the results back to the web test client in Visual Studio.
WebTestRequest request2 =
new WebTestRequest("https://" + serverName + "/reservations/index.aspx");
request2.Method = "POST";
BindHiddenFields request2BindHiddenFields =
new Microsoft.VisualStudio.QualityTools.WebTestFramework.BindHiddenFields();
request2BindHiddenFields.HiddenFieldGroup = "1";
request2.PreRequest
+= new EventHandler<PreRequestEventArgs>(request2BindHiddenFields.PreRequest);
FormPostHttpBody request2Body = new FormPostHttpBody();
request2Body.FormPostParameters.Add("LocationTime_ascx1:PickupLocationTextBox",
ConfigurationSettings.AppSettings["DefaultLocationCode"]);
request2Body.FormPostParameters.Add
("LocationTime_ascx1:PickupMonthYearDropDownList",
DateTime.Now.AddDays(1).ToString("MM/yyyy"));
request2Body.FormPostParameters.Add("LocationTime_ascx1:PickupDayDropDownList",
DateTime.Now.AddDays(1).Day.ToString());
request2Body.FormPostParameters.Add("LocationTime_ascx1:PickupTimeDropDownList",
"09:00");
request2Body.FormPostParameters.Add
("LocationTime_ascx1:ReturnMonthYearDropDownList",
DateTime.Now.AddDays(2).ToString("MM/yyyy"));
request2Body.FormPostParameters.Add("LocationTime_ascx1:ReturnDayDropDownList",
DateTime.Now.AddDays(2).Day.ToString());
request2Body.FormPostParameters.Add("LocationTime_ascx1:ReturnTimeDropDownList",
"09:00");
request2Body.FormPostParameters.Add("LocationTime_ascx1:VehicleTypeDropDownList",
"FullSize");
request2Body.FormPostParameters.Add
("LocationTime_ascx1:CorporateDiscountNumberTextBox",
"");
request2Body.FormPostParameters.Add
("LocationTime_ascx1:PromotionCodeTextBox", "");
request2Body.FormPostParameters.Add("LocationTime_ascx1:BlueChipNumberTextBox",
"");
request2Body.FormPostParameters.Add("LocationTime_ascx1:SubmitButton.x", "11");
request2Body.FormPostParameters.Add("LocationTime_ascx1:SubmitButton.y", "10");
request2.Body = request2Body;
ValidationRuleFindText rule3 = new ValidationRuleFindText();
rule3.FindText = "Total Base Rate";
rule3.IgnoreCase = true;
rule3.PassIfTextFound = true;
request2.ValidateResponse += new
EventHandler<ValidationEventArgs>(rule3.Validate);
yield return request2;
This next block of code is similar. Here you can see the value of each asp.net control being posted back to the server. Lastly, a validation rule looks for the text Total Base Rate to verify that the rate was successful.
One of the best new features in Visual Studio 2005 is the Web Test client. This client allows you to record test scripts using Internet Explorer and then customize them with code. You can execute the tests with any kind of browser but you can only record them with the IE plugin. To create a new test project, look underneath test in the C# language group (VB for Marcus) in the new project wizard.
Once you have a new project stated click the Record New Test button. This will launch a browser. Click the record button and then go to the pages that you want to test (you can go to multiple pages). Once you are finished recording, click stop and the data will be brought into Visual Studio.
The key thing to note is that you can test any kind of web site using a Web Test ASP.NET 2.0, ASP.NET 1.1, classic ASP, PHP, Java, or whatever. If you are testing an ASP.NET site, the client automatically supports the parsing out of the viewstate hidden fields so that the sessions will remain unique when it is testing.
The Web Test client supports a lot of customization out of the box even without code. You can post custom headers, files, and querystring or post parameters.
To actually test that an expected result is present, simply add a Validation Rule. You can add a validation rule to any step of the process and you can add as many as you want. There are four types of Validation Rules, ValidationRuleFindText, ValidationRuleRequestTime, ValidationRuleRequiredAttributeValue, and ValidationRuleRequiredTag. The only one I have really figured out how to use yet is ValidationRuleFindText (there is very little documentation on this right now).
The ValidationRuleFindText simply confirms that a string exists somewhere in the text of a response. You can configure it to ignore case and to pass or fail based on finding the text For example, if you want to look for an error on your page, set PassIfTextFound to false and then set the FindText property to the text of your error.
Tomorrow I will discuss how you can customize these tests with code. For something like this it is better to see it than to read about it, so I will most likely be giving a demo on it soon.
Continuing with DataSource controls, we now move on to the XmlDataSource control. This perhaps could be an alternative to using our custom label controls, etc. There might also be opportunities to extend this control in the future. The XmlDataSource control allows you to bound a control to an XmlDocument (typically located in a file). This is paritcularly useful when the data is hierarchical. You can bind to things like the TreeView control or SiteMap. As with all of the other DataSource controls. This control supports caching as well.
To use the XmlDataSource control simply specify the path to a file using the DataFile property. You can then set use XPath commands to get the value of nodes. Take a look at the example below. It uses a repeater to display the contents of an XML file. To bind to a DataSource, you always use the DataSourceId property and set it to the name of the control providing the data (this is typically done for you by a wizard).
<asp:XmlDataSource runat="server" id="XmlDataSource1"
DataFile="ReservationLabels_en-US.xml" />
<asp:repeater runat="server" id="Repeater1" DataSourceId="XmlDataSource1">
<ItemTemplate>
<!-- Insert xhtml formatting here -->
<%# XPath ("ReservationsLabels/Label/@key") %>
<%# XPath ("ReservationsLabels/Label/@value") %>
</ItemTemplate>
</asp:repeater>
This is a simple example that would print out the values of the ResevationLabels_en-US.xml file. If you have a hierachical file you can even use repeaters inside repeaters to display child nodes. Note the new use of the XPath keyword inside the data binding blocks. This control will probably prove to be pretty useful. I see a lot of potential with it when it is extended.
In continuing our discussion on databinding, we will move to the ObjectDataSource. This is by far one of the best controls they have added. It allows you to bind data to controls from custom user objects. Typically in our case this would be used to bind our data to a web service call. It works much in the same way as the SqlDataSource boject. When you drag one onto the page, a SmartTag allows you to specify which object to bind to and then it prompts you to select which method to use for Select, Update, Insert, and Delete operations.
The ObjectDataSource supports many of the same parameters as the SqlDataSource. The SelectMethod property specifies which method to call to perform a select operation. The SelectParameters property contains all of the parameters to the SelectMethod (again these can be a control, session, profile, cookie, etc). If you want to support paging, then you will need to specify a method with SelectCountMethod. This method will be used to get the number of rows returned by a select. There are also matching properties for Insert, Update, and Delete.
Here is a simple example, that is used by CarDashboard. You can view all the code in Source Control at $/Thrifty.NETv2.0b2/WebAdministration.
<asp:ObjectDataSource ID="VehicleImageObjectDataSource" Runat="server"
TypeName="Thrifty.Components.Web.Locations.Resources.Locations"
SelectMethod="GetVehicleImages" SelectCountMethod="GetVehicleImagesCount"
EnableCaching="false"CacheDuration="180"
OnSelecting="VehicleImageObjectDataSource_Selecting">
</asp:ObjectDataSource>
I really haven't talked about this control, because a lot of other stuff touches on it, but I think it is now time. As you know the existing DataGridcontrol is a big pile of dog crap. It gets you close to providing a grid, but it you want to do anything like editing, sorting, or paging, well have fun. The GridView makes our lives easier because it truly supports these things out of the box with very little code.
GridViews can be completely configured by using SmartTags. When you drop a GridView control into the IDE, a SmartTag is available which allows you to configure a DataSource. All data bindable controls now support the concept of binding to DataSource controls. These new controls make it extremely easy to bind a control to a stored procedure, object, or XML file. The controls that handle this are SqlDataSource, ObjectDataSource, and XmlDataSource.
In this article, I will start with the SqlDataSource. Although we rarely write code that connects directly to a SQL Server without a web service, knowing how to use this control will help you on your side projects when you are just trying to slap something together. The SmartTag will allow you to build an ad-hoc sql statement or specify a stored procedure to use. To use it, you start by specify a connection string to your SQL Server. If you haven't created one yet, the wizard will again allow you to create one. Using the connection string, it will query the database and see what tables are available as well as stored procedures.
That just takes care of a select statement etc (something to retrieve data). What makes the SqlDataSource control powerful is that it can also be used to seamlessly insert, update, and delete data. You can specify a SQL query or stored procedure for each. When you start using the advanced featured of the GridView or FormView control these will be used for automatic editing.
Once you specify a statement, you have to specify where the values to your stored procedure come from. These can be added at runtime if necessary, but that would require code. What's even better is that they can be configured through the wizard to automatically accept a value from a Cookie, Control, Form, Profile, QueryString, or Session. This allows a lot of flexibility in the datasource without ever having to write a line of code.
Do you want the data to be cached? No problem. Simply set EnableCaching to true. Then set a CacheDuration to the number of minutes, you want to cache for. It also supports SqlCacheDependencies for adavanced cache invalidation features provided by SQL Server 2005.
<asp:SqlDataSource ID="MasterSqlDataSource" Runat="server"
SelectCommand="GetErrorLogEntries" CacheDuration="60" EnableCaching="True"
SelectCommandType="StoredProcedure">
<SelectParameters>
<asp:ControlParameter Name="StartDateTime" Type="DateTime"
ControlID="StartDateTimeTextBox"
PropertyName="Text"></asp:ControlParameter>
<asp:ControlParameter Name="EndDateTime" Type="DateTime"
ControlID="EndDateTimeTextBox"
PropertyName="Text"></asp:ControlParameter>
<asp:ControlParameter Name="ServerName" Type="String"
ControlID="ServerNameDropDownList"
PropertyName="SelectedValue"></asp:ControlParameter>
<asp:ControlParameter ConvertEmptyStringToNull="False" Name="SearchString"
Type="String"
ControlID="SearchStringTextBox"
PropertyName="Text"></asp:ControlParameter>
</SelectParameters>
</asp:SqlDataSource>
Here is an example of a SqlDataSource used in the Error Log Viewer. In this case it pulls the values it needs from the four textbox controls on the page.
I'll be covering DataSource controls and the GridView more over the next couple of days. If you want to examine some actual code in use, take a look at the Error Log application. It is in source control at $/Thrifty.NETv2.0b2/WebAdministration/Diagnostics.
This is a well known feature of ASP.NET 2.0, but I have yet to talk about how it works. ASP.NET 2.0 now supports posting to another page (i.e.: Page1.aspx can post to Page2.aspx). The page that is posted to can access anything from the first page (i.e.: controls, etc). To specify a page to post back to, start by specifying a PostBackUrl on your submit button (this could be a Button, ImageButton, or LinkButton). In the page that you post to, if you need to check to see if this is a cross-page postback, you can check the value of the IsCrossPagePostBack property in the Page class.
As I said earlier, you can access controls from the calling page. Unfortunately, you have to do it through the use of FindControl(), which is why I would never be convinced to build an application with these. The Page class has a PreviousPage property which can then be used to find the control.
if (Page.IsCrossPagePostBack)
{
TextBox PickupLocationTextBox =
(TextBox)Page.PreviousPage.FindControls("PickupLocationTextBox");
}
Honestly, I don't care to go through all of that to get the value of a TextBox. If you ever have to though, that is how you can do it.
The pages node of the web.config contains a lot of new settngs that can be used to set compiler directives globally. Beside from the usual stuff such as Disabling Session State and View State, it has tags to specify a master page for all pages as well as the ability to set base classes for both pages and controls. To specify an inherited page or control, simply use the pageBaseType or userControlBaseType properties respectively. Here is the list of attributes the node supports now. Also as mentioned in a previous post, you can globally register a control or collection of controls from an assembly by using the registerTagPrefixes node.
<pages
buffer="true|false" enableSessionState="true|false|ReadOnly"
enableViewState="true|false"
enableViewStateMac="true|false"
autoEventWireup="true|false"
master="file path"
smartNavigation="true|false"
pageBaseType="typename, assembly"
userControlBaseType="typename"
validateRequest="true|false">
<imports>
<add namespace="namespace"/>
<remove namespace="namespace"/>
<clear/>
</imports>
<registerTagPrefixes>
<add tagPrefix="MyTags" namespace="MyNamespace"/>
</registertagPrefixes>
</pages>