Introduction
Using BizUnit to test complex testing cases is a common practice that we all have been using for a while now. This very useful library of testing steps provides ease in writing testing cases maintaining them and executing them. One common challenge commonly faced is how to call web services or more commonly how to call WCF services with a test step? There has been an out of the box test step to call SOAP 1.1 web services or WCF services based on BasicHttpBinding. Also there has been another custom one (here) to call a WCF with a common strongly typed contract. Of course there is always the good old HTTP step to invoke everything yourself and complicate your life.
In this post I will show you a custom test step that I have written to call a WCF dynamically without knowing anything about its contract. I am basing my code below on the out of the box Web service testing case already in BizUnit 4.0.
Solution
So like I said I wanted to call any WCF service with any binding and any contract without limitations. Also I wanted the flexibility to configure the service as I want using the app.config file.
So I have extracted the out of the box Web testing case and started to look into how to customize that test case. So here is what I did. I started with adding couple of properties for my testing case as below:
Property | Description |
DataLoaderBase RequestBody | This is the standard request body loader of the Web service call test step. |
string ServiceUrl | This is the URL of the WCF service you want to call. |
string Username | This is the username of the user to be added to the client credentials if the security mode is set to username. Else you should not specify this value. |
string Password | This is the password of the user to be added to the client credentials if the security mode is set to username. Else you should not specify this value. |
string BindingTypeName | This is the full type name of the binding required to be used when calling the WCF service. |
MessageVersion MsgVersion | This is the request message version to be used when calling the WCF service. |
string BindingConfigurationName | This is the name of the binding configuration in the app.config file. |
Then I validated my test step as per the below method:
if (string.IsNullOrEmpty(ServiceUrl))
{
thrownewStepValidationException("ServiceUrl may not be null or empty", this);
}
if (string.IsNullOrEmpty(Action))
{
thrownewStepValidationException("Action may not be null or empty", this);
}
if (string.IsNullOrEmpty(BindingTypeName))
{
thrownewStepValidationException("Binding type name may not be null or empty", this);
}
if (string.IsNullOrEmpty(BindingConfigurationName))
{
thrownewStepValidationException("Binding configuration name may not be null or empty", this);
}
RequestBody.Validate(context);
And then I started working on the Execute method. First thing I wanted is to create the binding and I used the reflection to do this and I used the binding configuration in the configuration file to customize the binding as I want.
Type bindingType = Type.GetType(BindingTypeName);
Binding binding = (Binding)Activator.CreateInstance(bindingType, BindingConfigurationName);
Then once I have the binding I created the address as below:
var epa = newEndpointAddress(newUri(serviceUrl));
I also created a dummy WCF service contract so that it would be a generic contract for any WCF service as below:
///<summary>
/// A dummy WCF interface that will be manipulated by the CallWebMethod above
///</summary>
[ServiceContract]
interfacegenericContract
{
[OperationContract(Action = "*", ReplyAction = "*")]
Message Invoke(Message msg);
}
Then I created the ChannelFactory using the EndpointAddress and the Binding created above as below:
cf = newChannelFactory<genericContract>(binding, epa);
One final note is that I used the Message version property to control which message version my WCF is using when I am creating the request message as below:
request = Message.CreateMessage(MsgVersion, action, r);
The remaining code is standard with no changes. Then I started to call my service as below:
var testCase = newTestCase();
var wcftststep = newWcfGenericRequestResponseStep();
wcftststep.ServiceUrl = "http://localhost:16987/Service1.svc";
wcftststep.Action = "http://tempuri.org/IService1/GetData";
wcftststep.BindingConfigurationName = "WSHttpBinding_IService1";
wcftststep.BindingTypeName = typeof(System.ServiceModel.WSHttpBinding).AssemblyQualifiedName;
wcftststep.FailOnError = true;
wcftststep.RunConcurrently = false;
wcftststep.MsgVersion = System.ServiceModel.Channels.MessageVersion.Soap12WSAddressing10;
wcftststep.RequestBody = newFileDataLoader()
{
FilePath = @"SampleInput.xml"
};
var xmlvalstep = newXmlValidationStep();
xmlvalstep.XmlSchemas.Add(newSchemaDefinition()
{
XmlSchemaPath = @"OutputSchema.xsd",
XmlSchemaNameSpace = @"http://tempuri.org/"
});
xmlvalstep.XPathValidations.Add(new BizUnit.TestSteps.Common.XPathDefinition()
{
XPath = "/*[local-name()='GetDataResponse' and namespace-uri()='http://tempuri.org/']/*[local-name()='GetDataResult' and namespace-uri()='http://tempuri.org/']",
Value = "You entered: 0"
});
wcftststep.SubSteps.Add(xmlvalstep);
testCase.ExecutionSteps.Add(wcftststep);
var bu = new BizUnit.BizUnit(testCase);
bu.RunTest();
The complete code for the WcfGenericRequestResponseStep is listed below:
using BizUnit;
using BizUnit.TestSteps.Common;
using BizUnit.TestSteps.Soap;
using BizUnit.Xaml;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace BizUnitTester
{
publicclassWcfGenericRequestResponseStep : TestStepBase
{
privateStream _request;
privateStream _response;
privateCollection<SoapHeader> _soapHeaders = newCollection<SoapHeader>();
publicDataLoaderBase RequestBody { get; set; }
publicstring ServiceUrl { get; set; }
publicstring Action { get; set; }
publicstring Username { get; set; }
publicstring Password { get; set; }
publicstring BindingTypeName { get; set; }
publicMessageVersion MsgVersion { get; set; }
publicstring BindingConfigurationName { get; set; }
public WcfGenericRequestResponseStep()
{
SubSteps = newCollection<SubStepBase>();
}
publicCollection<SoapHeader> SoapHeaders
{
set
{
_soapHeaders = value;
}
get
{
return _soapHeaders;
}
}
publicoverridevoid Execute(Context context)
{
_request = RequestBody.Load(context);
context.LogXmlData("Request", _request, true);
_response = CallWebMethod(
_request,
ServiceUrl,
Action,
Username,
Password,
context);
Stream responseForPostProcessing = _response;
foreach(var subStep in SubSteps)
{
responseForPostProcessing = subStep.Execute(responseForPostProcessing, context);
}
}
publicoverridevoid Validate(Context context)
{
if (string.IsNullOrEmpty(ServiceUrl))
{
thrownewStepValidationException("ServiceUrl may not be null or empty", this);
}
if (string.IsNullOrEmpty(Action))
{
thrownewStepValidationException("Action may not be null or empty", this);
}
if (string.IsNullOrEmpty(BindingTypeName))
{
thrownewStepValidationException("Binding type name may not be null or empty", this);
}
if (string.IsNullOrEmpty(BindingConfigurationName))
{
thrownewStepValidationException("Binding configuration name may not be null or empty", this);
}
RequestBody.Validate(context);
}
privateStream CallWebMethod(
Stream requestData,
string serviceUrl,
string action,
string username,
string password,
Context ctx )
{
try
{
Stream responseData;
Type bindingType = Type.GetType(BindingTypeName);
Binding binding = (Binding)Activator.CreateInstance(bindingType, BindingConfigurationName);
var epa = newEndpointAddress(newUri(serviceUrl));
ChannelFactory<genericContract> cf = null;
genericContract channel;
Message request;
Message response;
string responseString;
try
{
cf = newChannelFactory<genericContract>(binding, epa);
if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password))
{
cf.Credentials.UserName.UserName = username;
cf.Credentials.UserName.Password = password;
}
cf.Open();
channel = cf.CreateChannel();
using (newOperationContextScope((IContextChannel)channel))
{
XmlReader r = newXmlTextReader(requestData);
request = Message.CreateMessage(MsgVersion, action, r);
foreach (var header in _soapHeaders)
{
MessageHeader messageHeader = MessageHeader.CreateHeader(header.HeaderName, header.HeaderNameSpace, header.HeaderInstance);
OperationContext.Current.OutgoingMessageHeaders.Add(messageHeader);
}
response = channel.Invoke(request);
string responseStr = response.GetReaderAtBodyContents().ReadOuterXml();
ctx.LogXmlData("Response", responseStr);
responseData = StreamHelper.LoadMemoryStream(responseStr);
}
request.Close();
response.Close();
cf.Close();
}
catch (CommunicationException ce)
{
ctx.LogException(ce);
if (cf != null)
{
cf.Abort();
}
throw;
}
catch (TimeoutException te)
{
ctx.LogException(te);
if (cf != null)
{
cf.Abort();
}
throw;
}
catch (Exception e)
{
ctx.LogException(e);
if (cf != null)
{
cf.Abort();
}
throw;
}
return responseData;
}
catch (Exception ex)
{
ctx.LogException(ex);
throw;
}
}
///<summary>
/// A dummy WCF interface that will be manipulated by the CallWebMethod above
///</summary>
[ServiceContract]
interfacegenericContract
{
[OperationContract(Action = "*", ReplyAction = "*")]
Message Invoke(Message msg);
}
}
}