Calling a WCF dynamically using a BizUnit Test Step

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))
{
    throw new StepValidationException("ServiceUrl may not be null or empty", this);
}
 
if (string.IsNullOrEmpty(Action))
{
    throw new StepValidationException("Action may not be null or empty", this);
}
 
if (string.IsNullOrEmpty(BindingTypeName))
{
    throw new StepValidationException("Binding type name may not be null or empty", this);
}
 
if (string.IsNullOrEmpty(BindingConfigurationName))
{
    throw new StepValidationException("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 = new EndpointAddress(new Uri(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]
interface genericContract
{
    [OperationContract(Action = "*", ReplyAction = "*")]
    Message Invoke(Message msg);
}
 
Then I created the ChannelFactory using the EndpointAddress and the Binding created above as below:
cf = new ChannelFactory<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 = new TestCase();
 
var wcftststep = new WcfGenericRequestResponseStep();
wcftststep.ServiceUrl = "http://localhost:16987/Service1.svc&quot;;
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 = new FileDataLoader()
{
    FilePath = @"SampleInput.xml"
};
var xmlvalstep = new XmlValidationStep();
xmlvalstep.XmlSchemas.Add(new SchemaDefinition()
    {
        XmlSchemaPath = @"OutputSchema.xsd",
        XmlSchemaNameSpace = @"http://tempuri.org/&quot;
    });
xmlvalstep.XPathValidations.Add(new BizUnit.TestSteps.Common.XPathDefinition()
    {
        XPath = "/*[local-name()='GetDataResponse' and namespace-uri()='http://tempuri.org/'%5D/*%5Blocal-name()='GetDataResult&#039; and namespace-uri()='http://tempuri.org/'%5D&quot;,
        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
{
    public class WcfGenericRequestResponseStep : TestStepBase
    {
        private Stream _request;
        private Stream _response;
        private Collection<SoapHeader> _soapHeaders = new Collection<SoapHeader>();
 
        public DataLoaderBase RequestBody { get; set; }
        public string ServiceUrl { get; set; }
        public string Action { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public string BindingTypeName { get; set; }
        public MessageVersion MsgVersion { get; set; }
        public string BindingConfigurationName { get; set; }
 
        public WcfGenericRequestResponseStep()
        {
            SubSteps = new Collection<SubStepBase>();
        }
 
        public Collection<SoapHeader> SoapHeaders
        {
            set
            {
                _soapHeaders = value;
            }
            get
            {
                return _soapHeaders;
            }
        }
 
        public override void 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);
            }
        }
 
        public override void Validate(Context context)
        {
            if (string.IsNullOrEmpty(ServiceUrl))
            {
                throw new StepValidationException("ServiceUrl may not be null or empty", this);
            }
 
            if (string.IsNullOrEmpty(Action))
            {
                throw new StepValidationException("Action may not be null or empty", this);
            }
 
            if (string.IsNullOrEmpty(BindingTypeName))
            {
                throw new StepValidationException("Binding type name may not be null or empty", this);
            }
 
            if (string.IsNullOrEmpty(BindingConfigurationName))
            {
                throw new StepValidationException("Binding configuration name may not be null or empty", this);
            }
 
            RequestBody.Validate(context);
        }
 
        private Stream 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 = new EndpointAddress(new Uri(serviceUrl));
 
                ChannelFactory<genericContract> cf = null;
                genericContract channel;
                Message request;
                Message response;
                string responseString;
 
                try
                {
                    cf = new ChannelFactory<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 (new OperationContextScope((IContextChannel)channel))
                    {
                        XmlReader r = new XmlTextReader(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]
        interface genericContract
        {
            [OperationContract(Action = "*", ReplyAction = "*")]
            Message Invoke(Message msg);
        }
    }
}