I recently started implementing BDD using Specflow for one of our customers.
Basically Specflow is a BDD framework written in .NET inspired by Cucumber and the tests are written in the same language (Gherkin)
One of the things we would like to include in this are the testing of soap service endpoints.
If you are a .NET developer you probably know all about the generation of proxy classes by using the wizard in visual studio or invoking wsdl.exe on the wsdl source file. This could be a problem and result in needless work, so I looked into compiling the proxy classes in memory during runtime.
Authentication
There exist a number of free and open soap web services out there, some that are used in my examples are taken from www.webservicex.net.
But of course we do have services that require some form of authentication. The following snippet illustrates how to perform forms authentication using the System.Net.WebClient and Babelfish.
public string FormsAuth(Uri url, string username, string password)
{
var response = DownloadData(url);
INode document = new Babelfish.HTML.HTMLDocument(Encoding.ASCII.GetString(response));
var postData = string.Empty;
var formElements = document.Find(n => n.Name.ToLower() == "input" && new[] { "hidden", "button", "submit", "checkbox", "radio", "text", "password" }
.Contains(n.Attributes["type"]));
foreach (var formElement in formElements)
{
postData += postData.Length > 0 ? "&" : string.Empty;
var name = formElement.Attributes["name"];
var value = formElement.Attributes["value"];
switch (formElement.Attributes["type"])
{
case "text":
postData += string.Format("{0}={1}", name, HttpUtility.UrlEncode(username));
break;
case "password":
postData += string.Format("{0}={1}", name, HttpUtility.UrlEncode(password));
break;
default:
if (!string.IsNullOrEmpty(value))
postData += string.Format("{0}={1}", name, HttpUtility.UrlEncode(value));
break;
}
}
Headers.Add("Content-Type", "application/x-www-form-urlencoded");
string result;
try
{
result = Encoding.ASCII.GetString(UploadData(url, "POST", Encoding.ASCII.GetBytes(postData)));
}
catch (WebException ex)
{
result = ex.ToString();
}
return result;
}
Web Service instantiation
Now when we have access to the wsdl definition via our modified WebClient we can use that to compile a SoapHttpClientProtocol object.
The following snippet is from the class ArbitraryWebservice and shows how to compile a wsdl stream into such an object given a valid type.
private static SoapHttpClientProtocol GenerateService(Stream stream, string serviceName)
{
var serviceDescription = ServiceDescription.Read(stream);
var serviceDescriptionImporter = new ServiceDescriptionImporter
{
ProtocolName = "Soap12",
Style = ServiceDescriptionImportStyle.Client,
CodeGenerationOptions = CodeGenerationOptions.GenerateProperties
};
serviceDescriptionImporter.AddServiceDescription(serviceDescription, null, null);
var codeNamespace = new CodeNamespace();
var codeCompileUnit = new CodeCompileUnit();
codeCompileUnit.Namespaces.Add(codeNamespace);
var warnings = serviceDescriptionImporter.Import(codeNamespace, codeCompileUnit);
if (warnings > 0)
throw new Exception(string.Format("{0}", warnings));
using (var codeDomProvider = CodeDomProvider.CreateProvider("C#"))
{
var compilerParameters = new CompilerParameters(new[] { "System.dll", "System.Web.Services.dll", "System.Web.dll", "System.Xml.dll", "System.Data.dll" }) { GenerateInMemory = true };
var results = codeDomProvider.CompileAssemblyFromDom(compilerParameters, codeCompileUnit);
if (results.Errors.Count > 0)
throw new Exception(string.Join(", ", results.Errors.Cast<CompilerError>().Select(e => e.ToString()).ToArray()));
var service = results.CompiledAssembly.CreateInstance(serviceName);
return service as SoapHttpClientProtocol;
}
}
Method invocation
The following snippet is also from the class ArbitraryWebservice.
Basically it attempts to map the given methodName to a method in the instantated service object. Then it iterates trough the parameters for that method and match each one to one of the elements in the args array.
The last part is to cast/convert complex parameter types to the correct type using a extension method (basically JSON serialization/deserialization).
public object Call(string methodName, MethodParamDescriptor[] args)
{
var method = _service.GetType().GetMethod(methodName);
if (method == null)
throw new MissingMethodException(string.Format("this service contains no method named {0}", methodName));
var methodParams = new List<object>();
foreach (var p in method.GetParameters())
{
if(!args.Any(a => a.Name == p.Name))
throw new ArgumentException("expected parameter not supplied", p.Name);
methodParams.Add(args.Where(a => a.Name == p.Name)
.Select(a => a.Value.Convert(p.ParameterType))
.FirstOrDefault());
}
return method.Invoke(_service, methodParams.ToArray());
}
Specflow
Finally with the above in place we can write feature descriptions using Specflow and execute them as a normal unit test.
The complex object comparisons are done with the help of a Babelfish extension method
You can compile and run these tests yourself by downloading the solution from here.
Feature: Webservices
In order to know what is going on
As a developer
I want to instantiate any arbitrary web service and invoke availible methods and assert their return values
@mytag
Scenario: Weather
Given webservice named Weather with the wsdl url http://ws.cdyne.com/WeatherWS/Weather.asmx?wsdl
When I call the method GetCityForecastByZIP with the parameter ZIP=36003
Then I expect it to return an object matching content {City: 'Autaugaville', WeatherStationCity: 'Selma'}
@mytag
Scenario: Speed
Given webservice named ConvertSpeeds with the wsdl url http://www.webservicex.net/ConvertSpeed.asmx?WSDL
When I call the method ConvertSpeed with the parameters speed=100 and FromUnit=centimetersPersecond and ToUnit=feetPersecond
Then I expect it to return the number 3.2808398950131235
@mytag
Scenario: Whois
Given webservice named whois with the wsdl url http://www.webservicex.net/whois.asmx?WSDL
When I call the method GetWhoIS with the parameters HostName=google.com
Then I expect it to return a string containing TERMS OF USE: You are not authorized to access or query our Whois
@mytag
Scenario: country
Given webservice named country with the wsdl url http://www.webservicex.net/country.asmx?WSDL
When I call the method GetCountries with no parameters
Then I expect it to return html containing <NewDataSet><Table><Name>Kiribati</Name></Table><Table><Name>Cocos (Keeling) Islands</Name></Table></NewDataSet>
@mytag
Scenario: GeoIPService
Given webservice named GeoIPService with the wsdl url http://www.webservicex.net/geoipservice.asmx?WSDL
When I call the method GetGeoIP with the parameters IPAddress='209.85.148.147'
Then I expect it to return an object matching content {IP:'209.85.148.147', CountryName:'United States', CountryCode:'USA'}
@mytag
Scenario: BarCode
Given webservice named BarCode with the wsdl url http://www.webservicex.net/genericbarcode.asmx?WSDL
When I call the method GenerateBarCode with the parameters BarCodeText=123456 and BarCodeParam={Height:1,Width:1,Ratio:5,FontName:'Arial',FontSize:10,BarCodeImageFormat: 'PNG'}
Then I expect it to return a base64 string matching iVBORw0KGgoAAAANSUhEU...AAAAAAAAAAAAAAAAAAAAAAAAAAAAA==












