Announcing the ChannelAdam Nancy SOAP Library

My last article introduced the ChannelAdam SOAP Library to make it as easy as possible to create a SOAP 1.1 or SOAP 1.2 payload.

Also as mentioned in that last article, the .NET library Nancy is an excellent lightweight web framework for building HTTP-based web services. While Nancy will not itself ship support for SOAP it still can be used for hosting SOAP web services, if you can create the SOAP payload - and that is exactly what the ChannelAdam SOAP Library helps you do.

However, even after you use the ChannelAdam SOAP Library to build your SOAP payloads, there are still a few hurdles to jump over in order configure Nancy to work with SOAP HTTP requests… until now!

Announcing the release of the open source ChannelAdam Nancy SOAP Library which provides an adapter that makes it as easy as possible to work with SOAP 1.1 and SOAP 1.2 payloads in Nancy.

For information on how to get started, please see the usage documentation.

Enjoy!

The ChannelAdam Nancy Library

Overview

This is an open source (Apache License 2.0) .NET Standard 2.0 code library that provides some helpers and extensions for the lightweight Nancy web framework.

This library provides a foundation for the ChannelAdam Nancy SOAP Library.

Getting Started

NuGet Package Installation

To install the ChannelAdam.Nancy NuGet package run the following command in the Package Manager Console:

1PM> Install-Package ChannelAdam.Nancy

Reference

NancyRequestHelper

The NancyRequestHelper static class provides a helper method for operating on a Nancy Request.

The NancyRequestHelper static class has one method:

  • string GetRequestBodyAsString(Request request) - to extract the body of the Nancy Request as a string

This is also surfaced as an extension method on the Nancy Request class for which you will need a using statement for ChannelAdam.Nancy.

NancyResponseHelper

The NancyResponseHelper static class provides a helper method for operating on a Nancy Response.

The NancyResponseHelper static class has one method:

  • string GetResponseContentAsString(Response response) - to extract the contents of the Nancy Response as a string

This is also surfaced as an extension method on the Nancy Response class for which you will need a using statement for ChannelAdam.Nancy.

NancyResponseFactory

The NancyResponseFactory static class provides helper methods for creating a Nancy Response.

The NancyResponseFactory static class has two methods:

  • Response CreateFromEmbeddedResource(string resourceName, HttpStatusCode httpStatusCode, string contentType) - to create a Nancy Response from an embedded resource

  • Response CreateFromString(string responseValue, HttpStatusCode httpStatusCode, string contentType) - to create a Nancy Response from a given string

The ChannelAdam Nancy Self-Hosting Library

Overview

This is an open source (Apache License 2.0) .NET Standard 2.0 code library that provides some helpers for self-hosting the lightweight Nancy web framework.

This library builds upon the official Nancy Self-Hosting Library.

Getting Started

NuGet Package Installation

To install the ChannelAdam.Nancy.Hosting.Self NuGet package run the following command in the Package Manager Console:

1PM> Install-Package ChannelAdam.Nancy.Hosting.Self

Reference

NancySelfHostFactory

The NancySelfHostFactory provides functionality for creating and starting a self-hosted instance of Nancy.

The NancySelfHostFactory static class has nine methods:

  • public static HostConfiguration CreateHostConfiguration() - creates a Nancy HostConfiguration with the RewriteLocalhost property set to false to ensure that elevated permissions are not required to start Nancy.

  • public static NancyHost CreateAndStartNancyHost(params Uri[] baseUris) - creates and starts a self-hosted instance of Nancy using the host configuration created by CreateHostConfiguration().

  • public static NancyHost CreateAndStartNancyHost(HostConfiguration hostConfig, params Uri[] baseUris) - creates and starts a self-hosted instance of Nancy using the specified host configuration.

  • public static NancyHost CreateAndStartNancyHost(INancyBootstrapper bootstrapper, params Uri[] baseUris) - creates and starts a self-hosted instance of Nancy using the specified bootstrapper.

  • public static NancyHost CreateAndStartNancyHost(INancyBootstrapper bootstrapper, HostConfiguration hostConfig, params Uri[] baseUris) - creates and starts a self-hosted instance of Nancy using the specified bootstrapper and host configuration.

  • public static NancyHost CreateAndStartNancyHostInBackgroundThread(params Uri[] baseUris) - creates and starts a self-hosted instance of Nancy - in a background thread - using the host configuration created by CreateHostConfiguration().

  • public static NancyHost CreateAndStartNancyHostInBackgroundThread(HostConfiguration hostConfig, params Uri[] baseUris) - creates and starts a self-hosted instance of Nancy - in a background thread - using the specified host configuration.

  • public static NancyHost CreateAndStartNancyHostInBackgroundThread(INancyBootstrapper bootstrapper, params Uri[] baseUris) - creates and starts a self-hosted instance of Nancy - in a background thread - using the specified bootstrapper.

  • public static NancyHost CreateAndStartNancyHostInBackgroundThread(INancyBootstrapper bootstrapper, HostConfiguration hostConfig, params Uri[] baseUris) - creates and starts a self-hosted instance of Nancy - in a background thread - using the specified bootstrapper and host configuration.

The ChannelAdam Nancy SOAP Library

Overview

This is an open source (Apache License 2.0) .NET Standard 2.0 code library that provides functionality for working with SOAP 1.1 and SOAP 1.2 payloads in the lightweight Nancy web framework.

This library builds upon the foundational ChannelAdam Nancy Library and is complemented by:

Getting Started

NuGet Package Installation

To install the ChannelAdam.Nancy.Soap NuGet package run the following command in the Package Manager Console:

1PM> Install-Package ChannelAdam.Nancy.Soap

Usage

Example Hello World SOAP Nancy Module

Below is a simple example of a Nancy Module that defines a handler for a SOAP action.

 1public class HelloWorldSoapNancyModule : NancyModule
 2{
 3    public HelloWorldSoapNancyModule(INancySoapAdapter soapAdapter)
 4    {
 5        const string helloWorldSoapServiceRoutePattern = "/HelloWorldSoap11Service";
 6
 7        // Define the route pattern in Nancy
 8        DefineSoapRoute(helloWorldSoapServiceRoutePattern, soapAdapter);
 9
10        // Register SOAP action handlers for that route pattern
11        soapAdapter.RegisterSoapActionHandler(helloWorldSoapServiceRoutePattern, "urn:HelloWorldSoap11Service#HelloWorldSoapAction",
12            (request, routeArgs) =>
13                Soap11NancyResponseFactory.Create(
14                    SoapBuilder.CreateSoap11Envelope().WithBody.AddEntry("<root>Hello SOAP World!</root>"),
15                    HttpStatusCode.OK));
16
17        // And register more SOAP action handlers for the same route pattern:
18        // soapAdapter.RegisterSoapActionHandler(helloWorldSoapServiceRoutePattern, "urn:HelloWorldSoap11Service#AnotherSoapAction", ...
19    }
20
21    private void DefineSoapRoute(string routePattern, INancySoapAdapter soapAdapter)
22    {
23        Post[routePattern] = args => soapAdapter.ProcessRequest(routePattern, base.Request, args);
24    }
25}

There are three important things going on here:

  1. The ChannelAdam.Nancy.Soap.INancySoapAdapter is injected into the constructor of the Nancy Module class. Nancy’s default bootstrapper will automatically resolve the INancySoapAdapter soapAdapter parameter in the constructor, so you don’t need to be concerned about the constructor injection.

  2. As one normally would do, the route is defined. For a SOAP action, typically that would be done with a HTTP Post. Here is where it gets interesting. For a given route pattern, the action argument to specify is the INancySoapAdapter.ProcessRequest method. This ProcessRequest method on the SOAP Adapter conveniently extracts the SOAP action from either the HTTP header or the SOAP header and then dispatches the request to the corresponding SOAP action handler method that has been registered with it (see the next point).

  3. A SOAP action handler for the same route pattern is registered with the SOAP Adapter. We use the Soap11NancyResponseFactory.Create() method to convert the result of the SoapBuilder (or any string) into a Nancy Response with the appropriate content type for the version of SOAP you are using. Of course, there is also a Soap12NancyResponseFactory helper class for SOAP 1.2 responses.

Example of Self-Hosting Nancy in an Automated Test

Sometimes you want to mock out your actual SOAP services so you can perform Unit Testing instead of Integration Testing. With the ChannelAdam Nancy SOAP Adapter, it is possible to self-host Nancy in the same process as your test case, and just like a mock object, configure it to respond with a specific payload for the current Unit Test.

Below is some dirty automated test code (meant only for this purpose of demonstrating the simplest usage).

Note: I highly recommend following Behaviour-Driven Development practices and the usage of SpecFlow for automated testing.

Step 1 - Identify the Routes and SOAP Actions

The first step is to identify the routes patterns and SOAP actions. Maybe you have these already defined in some constant classes somewhere and if so that’s perfect. For the purpose of this example though, I am putting them into the following static classes.

1public static class TestCaseRoutes
2{
3    public static readonly string MySoap11Service = "/MySoap11Service";
4}
5
6public static class TestCaseSoapActions
7{
8    public static readonly string MySoap11ServiceTestAction = "urn:MySoap11Service#TestAction";
9}

Step 2 - Define a Nancy Module

The next step for an automated test is to create a Nancy Module and define a route.

Unlike the example Hello World Nancy Module above, the difference for an automated test is that within the Nancy Module you would not register the SOAP action handler with the SOAP Adapter (as that will be done later in the test code). The only thing you need to do in the Nancy Module is define the routes.

 1public class TestCaseNancyModule : NancyModule
 2{
 3    public TestCaseNancyModule(INancySoapAdapter soapAdapter)
 4    {
 5        DefineSoapRoute(TestCaseRoutes.MySoap11Service, soapAdapter);
 6    }
 7
 8    private void DefineSoapRoute(string routePattern, INancySoapAdapter soapAdapter)
 9    {
10        Post[routePattern] = args => soapAdapter.ProcessRequest(routePattern, base.Request, args);
11    }
12}

Step 3 - Write the Test Code

Finally the last step is to write the test code.

 1private const string NancyBaseUrl = "http://localhost:8087";
 2
 3private NancySoapAdapter nancySoapAdapter;
 4private NancyHost nancyHost;
 5
 6private string expectedSoapBodyEntryXml;
 7
 8public void SimpleDirtyTestCode()
 9{
10    //------------
11    // Set up
12    //------------
13
14    // Self host Nancy
15    this.nancySoapAdapter = new NancySoapAdapter();
16    this.nancyHost = NancySelfHostFactory.CreateAndStartNancyHostInBackgroundThread(
17        new NancySoapAdapterBootstrapper(this.nancySoapAdapter), 
18        new Uri(NancyBaseUrl));
19
20    //------------
21    // Arrange
22    //------------
23    this.expectedSoapBodyEntryXml = "<root>Whatever you need for this test</root>";
24
25    // Register SOAP action handler for the route pattern
26    soapAdapter.RegisterSoapActionHandler(TestCaseRoutes.MySoap11Service, TestCaseSoapActions.MySoap11ServiceTestAction,
27        (request, routeArgs) =>
28        {
29            // Return a Nancy Response with whatever this specific test case requires...
30            // A SOAP body entry or Soap11NancyResponseFactory.CreateFault()... 
31            return Soap11NancyResponseFactory.Create(
32                SoapBuilder.CreateSoap11Envelope().WithBody.AddEntry(this.expectedSoapBodyEntryXml),
33                HttpStatusCode.OK);
34        });
35    
36    //------------
37    // Act
38    //------------
39    /// TODO - Invoke your System Under Test that will call the Nancy route with the specified SOAP action
40
41    //------------
42    // Assert
43    //------------
44    // TODO - assert the actual response equals this.expectedSoapBodyEntryXml
45    //      - or that a FaultException occurred...
46
47    //------------
48    // Tear Down
49    //------------
50    this.nancyHost.Stop();
51}

The important things to understand are:

  • With the help of the ChannelAdam.Nancy.SelfHosting.NancySelfHostingFactory class, we are self-hosting Nancy in the same process as the test case. Because of this, we specifically create and start it in a background thread so that if the main thread of the test case performs a HTTP request to the route, then everything will respond and be processed as expected.

  • We explicitly create the NancySoapAdapter in the test case and bootstrap it with the NancySoapAdapterBootstrapper into the factory method that creates and starts Nancy. This ensures that the Nancy Module constructor receives the SOAP Adapter instance that we created.

  • Using the NancySoapAdapter, we then register a SOAP action handler specifically returning the response required by the test case.

Reference

NancySoapAdapter & INancySoapAdapter

The ChannelAdam.Nancy.Soap.NancySoapAdapter class which implements the ChannelAdam.Nancy.Soap.Abstractions.INancySoapAdapter interface makes all the magic happen for adapting SOAP payloads to work easily in Nancy.

There are four methods in the interface:

  • Response ProcessRequest(string routePattern, Request request) - this is the action to specify in the Nancy route definition to dispatch the HTTP request to the registered handler for the SOAP action named in the HTTP request header or SOAP header.

  • Response ProcessRequest(string routePattern, Request request, dynamic requestRouteArgs) - this is the action to specify in the Nancy route definition to dispatch the HTTP request to the registered handler for the SOAP action named in the HTTP request header or SOAP header. This overload allows the requestRouteArgs from the route pattern to be provided.

  • void RegisterSoapActionHandler(string routePattern, string soapAction, NancyRequestHandler handler) - registers a handler for the specified route pattern and SOAP action.

  • Request TryWaitForRequest(string routePattern, string soapAction, int retryCount, TimeSpan sleepDuration) - Waits for a specified amount of time for a HTTP request to be received on the specified route with the given SOAP action.

NancySoapAdapterBootstrapper

The NancySoapAdapterBootstrapper is a custom bootstrapper for Nancy.

Normally you don’t need to be concerned with this, but it is especially useful in automated tests.

There are two constructors:

  • public NancySoapAdapterBootstrapper() - which will create a NancySoapAdapter instance for you.

  • public NancySoapAdapterBootstrapper(INancySoapAdapter soapAdapter) - which allows you to specify your own instance of the NancySoapAdapter (which is especially useful in automated tests).

Soap11NancyResponseFactory

The Soap11NancyResponseFactory static class allows you to create a Nancy Response with the correct content type for SOAP 1.1.

There are five methods on the static class:

  • public static Response Create(ISoap11EnvelopeBuilder envelopeBuilder, HttpStatusCode httpStatusCode) - to create a Nancy Response from the specified ISoap11EnvelopeBuilder with the correct content type for SOAP 1.1 and with the specified HttpStatusCode.

  • public static Response Create(XContainer soap11Envelope, HttpStatusCode httpStatusCode) - to create a Nancy Response from the specified SOAP 1.1 envelope in the XContainer with the correct content type for SOAP 1.1 and with the specified HttpStatusCode.

  • public static Response Create(string soap11EnvelopeXml, HttpStatusCode httpStatusCode) - to create a Nancy Response from the specified SOAP 1.1 envelope XML string with the correct content type for SOAP 1.1 and with the specified HttpStatusCode.

  • public static Response CreateFault(ISoap11EnvelopeBuilder envelopeBuilder) - to create a Nancy Response with a SOAP 1.1 fault, as specified by the ISoap11EnvelopeBuilder, with an HttpStatusCode.InternalServerError.

  • public static Response CreateFault(XContainer envelope) - to create a Nancy Response with a SOAP 1.1 fault, as specified by the SOAP 1.1 envelope in the specified XContainer, with an HttpStatusCode.InternalServerError.

Soap12NancyResponseFactory

The Soap12NancyResponseFactory static class allows you to create a Nancy Response with the correct content type for SOAP 1.2. This is the SOAP 1.2 equivalent of the Soap11NancyResponseFactory.

There are five methods on the static class:

  • public static Response Create(ISoap12EnvelopeBuilder envelopeBuilder, HttpStatusCode httpStatusCode) - to create a Nancy Response from the specified ISoap11EnvelopeBuilder with the correct content type for SOAP 1.2 and with the specified HttpStatusCode.

  • public static Response Create(XContainer soap12Envelope, HttpStatusCode httpStatusCode) - to create a Nancy Response from the specified SOAP 1.2 envelope in the XContainer with the correct content type for SOAP 1.2 and with the specified HttpStatusCode.

  • public static Response Create(string soap12EnvelopeXml, HttpStatusCode httpStatusCode) - to create a Nancy Response from the specified SOAP 1.2 envelope XML string with the correct content type for SOAP 1.2 and with the specified HttpStatusCode.

  • public static Response CreateFault(ISoap12EnvelopeBuilder envelopeBuilder) - to create a Nancy Response with a SOAP 1.2 fault, as specified by the ISoap11EnvelopeBuilder, with an HttpStatusCode.InternalServerError.

  • public static Response CreateFault(XContainer envelope) - to create a Nancy Response with a SOAP 1.2 fault, as specified by the SOAP 1.2 envelope in the specified XContainer, with an HttpStatusCode.InternalServerError.

SoapNancyRequestHelper

The SoapNancyRequestHelper static class provides helpful functionality that operates on a Nancy Request.

There is one method on the static class:

  • public static string GetSoapAction(Request request) - to retrieve the SOAP action from either the HTTP header or the SOAP envelope header. The HTTP header takes precedence.

This functionality is also surfaced as an extension method on the Nancy Request class for which you will need a using statement for ChannelAdam.Nancy.Soap.

Announcing the ChannelAdam SOAP Library

The Simple Object Access Protocol (SOAP) has been around for more than a decade. While HTTP-based API’s and RESTful services are the current trend, SOAP web services are still the rock bed of most businesses and contemporary systems.

On-premises API gateways such as the CA API Gateway has had support for publishing SOAP web services for quite a while.

In fact, there is still such demand in the industry for integration with SOAP services that Microsoft’s newer cloud-based Azure API Management service also now has the capability to publish SOAP API’s.

The .NET library Nancy is an excellent lightweight web framework for building HTTP-based web services. While Nancy will not itself ship support for SOAP it still can be used for hosting SOAP web services, if you can create the SOAP payload.

Libraries such as Nancy or Mock4Net (a tiny mock HTTP server for .NET) can be used in automated Unit Tests to dynamically create mock web services that return the exact response required for each specific test case. In fact, I personally use this specific technique to perform advanced Unit Testing of black-box code artefacts such as BizTalk orchestrations.

However, creating and composing a SOAP envelope, header, and body or fault has always been a tricky undertaking, and unless you have studied the SOAP specifications in detail and use them daily, it is easy to either forget or confuse the differences between SOAP 1.1 and SOAP 1.2 - that is, until now!

Introducing the ChannelAdam SOAP Library – a fluent .NET API to make it as easy as possible to create a SOAP 1.1 or SOAP 1.2 payload.

For information on how to get started, please see the usage documentation.

You might also be interested in the ChannelAdam Nancy SOAP Library which makes using SOAP with Nancy even easier.

The ChannelAdam SOAP Library

Overview

This is an open source (Apache License 2.0) .NET Standard 2.0 code library that provides a fluent API to make it as easy as possible to create a SOAP 1.1 or SOAP 1.2 message.

Among other things, it is perfect for building SOAP envelopes to use in lightweight web frameworks such as Nancy.

Getting Started

NuGet Package Installation

To install the ChannelAdam.Soap NuGet package run the following command in the Package Manager Console:

1PM> Install-Package ChannelAdam.Soap

Usage

There are plenty of overloads and other methods for more advanced usage - see the reference in the next section.

SOAP Envelope with a Header Action and Body

Here is a simple example of creating a SOAP 1.1 envelope with a header action and body payload.

1string envelopeXml = SoapBuilder.CreateSoap11Envelope()
2    .WithHeader.AddAction("http://my.action/here")
3    .WithBody.AddEntry("<myXml>hello from the body</myXml>")
4    .Build()
5    .ToString();

And for SOAP 1.2:

1string envelopeXml = SoapBuilder.CreateSoap12Envelope()
2    .WithHeader.AddAction("http://my.action/here")
3    .WithBody.AddEntry("<myXml>hello from the body</myXml>")
4    .Build()
5    .ToString();

There is even an overload to create the body entry from .NET objects. For instance:

 1public class MyModel
 2{
 3    public string Value { get; set; } 
 4}
 5
 6...
 7
 8var model = new MyModel { Value = "hello!" };
 9
10string envelopeXml = SoapBuilder.CreateSoap12Envelope()
11    .WithHeader.AddAction("http://my.action/here")
12    .WithBody.AddEntry(model)
13    .Build()
14    .ToString();

Easy!

SOAP Faults

To create a SOAP 1.1 fault:

1string envelopeXml = SoapBuilder.CreateSoap11Envelope()
2    .WithBody.SetFault(Soap11FaultCode.Client, "SOAP 1.1 fault string here")
3    .Build()
4    .ToString();

And for a SOAP 1.2 fault:

1string envelopeXml = SoapBuilder.CreateSoap11Envelope()
2    .WithBody.SetFault(Soap12FaultCode.Sender, "SOAP 1.2 fault reason here")
3    .Build()
4    .ToString();

There are overloads to set other SOAP fault value properties as well.

Reference

This reference assumes that the reader is familiar with SOAP terminology. For more information see the following SOAP specifications:

SoapBuilder

The SoapBuilder static class is the fluent starting point for building a SOAP 1.1 or SOAP 1.2 envelope.

The SoapBuilder static class has two methods:

  • ISoap11EnvelopeBuilder CreateSoap11Envelope() - to start the process of creating a SOAP 1.1 envelope

  • ISoap12EnvelopeBuilder CreateSoap12Envelope() - to start the process of creating a SOAP 1.2 envelope

The ISoap11EnvelopeBuilder interface has two properties and one method:

  • ISoap11BodyBuilder WithBody { get; } - to specify the SOAP body or a fault

  • ISoap11HeaderBuilder WithHeader { get; } - to specify the SOAP header

  • XContainer Build() - to finally build the SOAP envelope as specified

The ISoap12EnvelopeBuilder interface is similar:

  • ISoap12BodyBuilder WithBody { get; }

  • ISoap12HeaderBuilder WithHeader { get; }

  • XContainer Build()

Usage of SoapBuilder

The general procedure for usage is:

  1. Call one of the SoapBuilder.CreateSoap??Envelope() methods.

  2. If necessary, add a header via the WithHeader property.

  3. Add a body or fault message via the WithBody property.

  4. Once all the details have been specified, simply call the Build() method which will return an XContainer.

  5. If there is something that you need to do to the contents of the SOAP envelope that is not currently allowed by the API, then you can always directly modify the elements in the XContainer and do magical things with LINQ.

  6. A simple .ToString() on the XContainer returned from Build() will then give you the XML string of the SOAP envelope/payload - by default formatted with indentation. There is also the overload .ToString(System.Xml.Linq.SaveOptions options) that allows you to change the formatting of whitespace and indentation.

Specify the SOAP Header

The SOAP header can be specified via the WithHeader property.

For SOAP 1.1, WithHeader chains the following methods:

1ISoap11EnvelopeBuilder AddAction(string action);
2
3ISoap11EnvelopeBuilder AddBlock(string headerXml);
4ISoap11EnvelopeBuilder AddBlock(XContainer headerBlock);
5ISoap11EnvelopeBuilder AddBlock(object toSerialise);
6ISoap11EnvelopeBuilder AddBlock(object toSerialise, string toElementName, string toElementNamespace);
7
8ISoap11EnvelopeBuilder SetStandardSoapEncoding();
9ISoap11EnvelopeBuilder SetCustomSoapEncoding(string soapEncodingNamespace);

Notes:

  • AddAction(string action) adds an action header block with the specified action.
  • AddBlock(XContainer headerBlock) adds any arbitrary header block - e.g. from a System.Xml.Linq.XElement.
  • AddBlock(object toSerialise, string toElementName, string toElementNamespace) serialises the given object and during serialisation overrides the XML element name and namespace with those specified.
  • ISoap11EnvelopeBuilder SetStandardSoapEncoding() sets the SoapEncoding attribute to the standard encoding namespace
  • ISoap11EnvelopeBuilder SetCustomSoapEncoding(string soapEncodingNamespace) sets the SoapEncoding attribute to your custom namespace

For SOAP 1.2, WithHeader is nearly identical to SOAP 1.1, except an ISoap12EnvelopeBuilder is returned instead of a ISoap11EnvelopeBuilder.

Specify the SOAP Body Payload or Fault

The SOAP body payload or fault can be specified via the WithBody property which provides the following methods.

For SOAP 1.1

 1ISoap11EnvelopeBuilder AddEntry(string bodyXml);
 2ISoap11EnvelopeBuilder AddEntry(XContainer fromNode);
 3ISoap11EnvelopeBuilder AddEntry(object toSerialise);
 4ISoap11EnvelopeBuilder AddEntry(object toSerialise, string toElementName, string toElementNamespace);
 5
 6ISoap11EnvelopeBuilder SetFault(Soap11FaultCode code, string faultString);
 7ISoap11EnvelopeBuilder SetFault(Soap11FaultCode code, string faultString, string faultActor);
 8ISoap11EnvelopeBuilder SetFault(Soap11FaultCode code, string faultString, string faultActor, IEnumerable<XContainer> detailEntries);
 9
10ISoap11EnvelopeBuilder SetStandardSoapEncoding();
11ISoap11EnvelopeBuilder SetCustomSoapEncoding(string soapEncodingNamespace);

Notes:

  • AddEntry(object toSerialise, string toElementName, string toElementNamespace) serialises the given object and during serialisation overrides the XML element name and namespace with those specified.

SOAP 1.2 Faults are more descriptive than SOAP 1.1, hence the overloads of SetFault with more parameters.

For SOAP 1.2

 1ISoap12EnvelopeBuilder AddEntry(string bodyXml);
 2ISoap12EnvelopeBuilder AddEntry(XContainer fromNode);
 3ISoap12EnvelopeBuilder AddEntry(object toSerialise);
 4ISoap12EnvelopeBuilder AddEntry(object toSerialise, string toElementName, string toElementNamespace);
 5
 6ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, string reason);
 7ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, string subCode, string reason);
 8ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, XNamespace subCodeNamespace, string subCode, string reason);
 9ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, XNamespace subCodeNamespace, string subCode, string reason, string reasonXmlLanguage);
10ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, XNamespace subCodeNamespace, string subCode, string reason, string reasonXmlLanguage, string node);
11ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, XNamespace subCodeNamespace, string subCode, string reason, string reasonXmlLanguage, string node, string role);
12ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, XNamespace subCodeNamespace, string subCode, string reason, IEnumerable<XContainer> detailEntries);
13ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, XNamespace subCodeNamespace, string subCode, string reason, string reasonXmlLanguage, IEnumerable<XContainer> detailEntries);
14ISoap12EnvelopeBuilder SetFault(Soap12FaultCode code, XNamespace subCodeNamespace, string subCode, string reason, string reasonXmlLanguage, string node, string role, IEnumerable<XContainer> detailEntries);
15
16ISoap12EnvelopeBuilder SetStandardSoapEncoding();
17ISoap12EnvelopeBuilder SetCustomSoapEncoding(string soapEncodingNamespace);

The ChannelAdam Test Framework Libraries

IMPORTANT: DEPRECATION NOTICE

This documention is for Version 1 of these full .NET Framework libraries and it has been deprecated.

Please see the [Version 2] documentation for the following ChannelAdam .NET Standard libraries.

Overview

The open source ChannelAdam Test Framework Libraries are a set of automated testing libraries to make testing your .NET code easier.

There are currently 3 main libraries:

  • The ChannelAdam Test Framework - the common test framework library without a dependency on any other test runner or library
  • The ChannelAdam Test Framework for MSTest - containing MSTest-specific implementations of interfaces in the common library
  • The ChannelAdam Test Framework for BizTalk - containing functionality specifically for testing Microsoft BizTalk artifacts.

The ChannelAdam Test Framework Libraries are fully compatible with MSTest and .NET Framework 4.0 and above.

Note: I highly recommend using these libraries with Behaviour-Driven Development practices and the usage of SpecFlow.

Getting Started

NuGet Package Installation

The easiest way to get started is to install the library that suits your favourite test runner or test library. Currently however there is only the MSTest implementation…

To install the ChannelAdam.TestFramework.MSTest NuGet package run the following command in the Package Manager Console:

1PM> Install-Package ChannelAdam.TestFramework.MSTest

Features

Logger

Logging useful information to the output of a test is extremely important.

The ISimpleLogger interface in the ChannelAdam Core Library provides a very simple interface with a simple Log method that is intended for usage in scenarios such writing test output.

The SimpleConsoleLogger class implements ISimpleLogger and is a thin wrapper around Console.WriteLine().

Log and then Assert

I cannot emphasise enough how important it is to log useful to the output of a test. Developers and analysts almost should be able to read the output of a test like a story. Analysts especially should review the test outputs and be satisfied that the test was implemented correctly - almost as if they performed the test themselves. Therefore, when an assertion is performed, we want the test output to actually tell us that an assertion is taking place and provide a information about what is being asserted.

The Log Asserter interface provides all the usual assertion methods that you are familiar with, and the first parameter of all of the methods is a string that will be outputted to an ISimpleLogger (provided in the constructor) before performing the actual assertion.

This LogAssert class is the MSTest implementation of the Log Asserter interface.

Test Easy Base Class for Tests

Logging, assertions and testing for exceptions are 3 of some fundamental features common to most automated test suites. Wouldn’t it be nice to be able to perform that functionality easily and keep your test code to a minimum?

The TestEasy base class for tests provides those abilities.

This class provides the following helpful properties.

  • ISimpleLogger Logger - for logging to the test output

  • ILogAsserter LogAssert - your handy-dandy log asserter

  • ExpectedExceptionDescriptor ExpectedException - describes the exception that is expected to occur for a specific test case

  • Exception ActualException - for storing the actual exception that occurred

When the ActualException property is set, the details are automatically logged to the test output.

TestEasy also contains the following helpful methods:

  • void Try(Action action) - performs the given action and catches exceptions into ActualException

  • void Try(Func<Task> action) - performs the given asynchronous action that returns a Task, waits for it finish, and catches exceptions into ActualException

  • void AssertNoExceptionOccurred() - asserts that no ActualException exists/occurred

  • void AssertExpectedException(Type expectedExceptionType) - asserts that type of the value of ActualException is the given expected type

  • void AssertExpectedException() - asserts that the ActualException is equivalent to that described by the ExpectedException descriptor. When the ActualException is an System.AggregateException, the assertion will pass if there is at least one inner exception equivalent to the ExpectedException descriptor.

Expected Exception Descriptor

The ExpectedException property is implemented by the ExpectedExceptionDescriptor class. This class describes the exception that is expected to occur.

There are 2 properties in ExpectedExceptionDescriptor that are specifically used in the AssertExpectedException() method:

  • string MessageShouldContainText - which describes the substring that the ActualException.Message property should contain

  • Type ExpectedType - which describes the System.Type that ActualException should be.

Either or both of these properties can be set. When set, the details are automatically logged to the test output.

Moq Test Fixture Base Class for Tests

MoqTestFixture inherits all the goodies from TestEasy and also provides a MyMockRepositoy property to access the Moq MockRepository.

The default constructor automatically creates a Moq “loose” MockRepository, but that can be specified in an overload of the constructor.

Text Tester

The TextTester class is used in tests for asserting whether actual text is similar enough to the given expected text.

Under the covers, it uses the DiffPlex open source library to identify differences.

The TextTester class has the following properties:

  • string ActualText

  • string ExpectedText

  • DiffPaneModel Differences

  • Action<DiffPaneModel> TextDifferenceFilter - a filter for the diff (see below)

And the following methods:

  • void ArrangeActualText(Assembly assembly, string resourceName) - to arrange the actual text from an embedded resource in the given assembly

  • void ArrangeActualText(string text) - to arrange the actual text from the given string

  • void ArrangeExpectedText(Assembly assembly, string resourceName) - to arrange the expected text from an embedded resource in the given assembly

  • void ArrangeExpectedText(string text) - to arrange the expected text from the given string

  • virtual void AssertActualTextEqualsExpectedText() - to assert the actual text against the expected text

  • bool IsEqual() - determines if the given actual and expected text is equivalent

  • virtual bool IsEqual(string expected, string actual) - determines if the given actual and expected text is equivalent

Filtering Text Differences

The TextTester class provides 3 mechanisms for hooking into the DiffPlex DiffPaneModel difference result, allowing you to manipulate it before a final decision is made as
to whether or not the actual text is similar enough to the expected text.

  • An event that is raised with the differences
1    /// <summary>
2    /// Occurs when a text difference is detected, allowing a listener to filter the differences and
3    /// change the Line[x].Type to ChangeType.UnChanged so that the difference is no longer treated as a difference.
4    /// </summary>
5    /// <remarks>
6    /// This event or the TextDifferenceFilter property can be used for this purpose.
7    /// </remarks>
8    public event EventHandler<TextDifferenceDetectedEventArgs> TextDifferenceDetectedEvent;
  • An action delegate method that is called with the differences
1    /// <summary>
2    /// Gets or sets the Action delegate to be invoked when a text difference is detected, allowing differences to be filtered out by
3    /// changing the Line[x].Type to ChangeType.UnChanged - so that a difference is no longer treated as a difference.
4    /// </summary>
5    /// <value>The Action delegate.</value>
6    /// <remarks>
7    /// This property or TextDifferenceDetectedEvent can be used for this purpose.
8    /// </remarks>
9    public Action<DiffPaneModel> TextDifferenceFilter { get; set; }
  • A virtual method that allows you to inherit from the tester class and override the differences when they are detected.
1    protected virtual void OnTextDifferenceDetected(DiffPaneModel differences)

For a quick example, please see the Behaviour Specification for the TextTester class and the associated
text tester code

XML Tester

The XmlTester class is used in tests for asserting whether the actual XML is similar enough to the given expected XML.

Under the covers, it uses the XMLUnit.NET open source library to identify differences.

The XMLTester class has the following properties:

  • XElement ActualXml

  • XElement ExpectedXml

  • Diff Differences

And the following methods:

  • void ArrangeActualXml(Assembly assembly, string resourceName) - to arrange the actual XML from an embedded resource in the given assembly

  • void ArrangeActualXml(XElement xmlElement) - to arrange the actual XML from a given XElement

  • void ArrangeActualXml(object valueToSerialise) - to arrange the actual XML by serialising the given object

  • void ArrangeActualXml(object valueToSerialise, XmlRootAttribute xmlRootAttribute) - to arrange the actual XML by serialising the given object and applying the given XmlRootAttribute during the serialisation

  • void ArrangeActualXml(object valueToSerialise, XmlAttributeOverrides xmlAttributeOverrides) - to arrange the actual XML by serialising the given object and applying the given XmlAttributeOverrides during the serialisation

  • void ArrangeActualXml(string xmlValue) - to arrange the actual XML from the given XML string

  • void ArrangeExpectedXml(Assembly assembly, string resourceName) - to arrange the expected XML from an embedded resource in the given assembly

  • void ArrangeExpectedXml(XElement xmlElement) - to arrange the expected XML from a given XElement

  • void ArrangeExpectedXml(object valueToSerialise) - to arrange the expected XML by serialising the given object

  • void ArrangeExpectedXml(object valueToSerialise, XmlRootAttribute xmlRootAttribute) - to arrange the expected XML by serialising the given object and applying the given XmlRootAttribute during the serialisation

  • void ArrangeExpectedXml(object valueToSerialise, XmlAttributeOverrides xmlAttributeOverrides) - to arrange the expected XML by serialising the given object and applying the given XmlAttributeOverrides during the serialisation

  • void ArrangeExpectedXml(string xmlValue) - to arrange the expected XML from the given XML string

  • virtual void AssertActualXmlEqualsExpectedXml() - to assert the actual XML against the expected XML

  • virtual void AssertActualXmlEqualsExpectedXml(IXmlFilter xmlFilter) - to assert the actual XML against the expected XML, ignoring items listed in the given IXmlFilter (see below)

  • bool IsEqual() - determines if the given actual and expected XML is equivalent

  • bool IsEqual(XNode expected, XNode actual) - determines if the given actual and expected XNode XML is equivalent

  • virtual bool IsEqual(XmlNode expected, XmlNode actual) - determines if the given actual and expected XmlNode XML is equivalent

Ignoring / Filtering XML Elements

The AssertActualXmlEqualsExpectedXml(IXmlFilter xmlFilter) override allows you to specify an IXmlFilter interface which allows you to specify a list of the local names of XML elements to ignore in comparisons, and/or a list of XPath expressions to ignore in comparisons.

Examples

For a quick example, please see the Behaviour Specification for the XmlTester class and the XML tester code

XML Filter

The XmlFilter class allows you to specify a list of the local names of XML elements to ignore in comparisons, and/or a list of XPath expressions to ignore in comparisons.

Two constructor methods allow you to easily specify these lists.

  • XmlFilter(IList<string> elementLocalNamesToIgnore)

  • XmlFilter(IList<string> elementLocalNamesToIgnore, IList<string> xpathsToIgnore)

XML Asserter

The XmlAsserter class provides utility methods for performing assertions on XML values. It has the following methods:

  • void XPathValueEquals(string description, string xpath, XNode rootElement, string expected) - to assert that the given XPath of the provided XNode has the given expected string value

  • void XPathValueEquals(string description, string xpath, XNode rootElement, XmlNamespaceManager namespaceManager, string expected) - uses the provided namespace manager to assert that the given XPath of the provided XNode has the given expected string value

  • void XPathValuesAreEqual(string description, string xpath, XNode expectedElements, XNode actualElements) - to assert that the value of the XPath in the given actual XNode equals the corresponding value in the given expected XNode

  • void XPathValuesAreEqual(string description, string expectedXpath, XNode expectedElements, string actualXpath, XNode actualElements) - to assert that the value of the actual XPath in the actual XNode is the same as the value of the expected XPath in the expected XNode

  • void XPathValuesAreEqual(string description, string expectedXpath, XNode expectedElements, XmlNamespaceManager expectedNamespaceManager, string actualXpath, XNode actualElements, XmlNamespaceManager actualNamespaceManager) - to assert that the value of the actual XPath in the actual XNode is the same as the value of the expected XPath in the expected XNode, using the provided XML namespace managers

  • void AreEqual(XElement expectedXml, XElement actualXml) - to assert that the given expected XElement is equivalent to the actual XElement

  • void AreEqual(XElement expectedXml, XElement actualXml, IXmlFilter xmlFilter) - to assert that the given expected XElement is equivalent to the actual XElement - using the given IXmlFilter

Mapping Testers

There are 4 map tester classes to be used depending on the type of input and output of your map:

The MappingFromXmlToXmlTester class has the following method overrides for arranging the input XML.

  • void ArrangeInputXml(Assembly assembly, string resourceName) - to arrange the input XML from an embedded resource in the given assembly

  • void ArrangeInputXml(XElement xmlElement) - to arrange the input XML from a given XElement

  • void ArrangeInputXml(object valueToSerialise) - to arrange the input XML by serialising the given object

  • void ArrangeInputXml(object valueToSerialise, XmlRootAttribute xmlRootAttribute) - to arrange the input XML by serialising the given object and applying the given XmlRootAttribute during the serialisation

  • void ArrangeInputXml(object valueToSerialise, XmlAttributeOverrides xmlAttributeOverrides) - to arrange the input XML by serialising the given object and applying the given XmlAttributeOverrides during the serialisation

  • void ArrangeInputXml(string xmlValue) - to arrange the input XML from the given XML string

  • void ArrangeExpectedOutputXml(Assembly assembly, string resourceName) - to arrange the expected output XML from an embedded resource in the given assembly

  • void ArrangeExpectedOutputXml(XElement xmlElement) - to arrange the expected output XML from a given XElement

  • void ArrangeExpectedOutputXml(object valueToSerialise) - to arrange the expected output XML by serialising the given object

  • void ArrangeExpectedOutputXml(object valueToSerialise, XmlRootAttribute xmlRootAttribute) - to arrange the expected output XML by serialising the given object and applying the given XmlRootAttribute during the serialisation

  • void ArrangeExpectedOutputXml(object valueToSerialise, XmlAttributeOverrides xmlAttributeOverrides) - to arrange the expected output XML by serialising the given object and applying the given XmlAttributeOverrides during the serialisation

  • void ArrangeExpectedOutputXml(string xmlValue) - to arrange the expected output XML from the given XML string

Input can be arranged from an embedded resource, XElement, object that is XML serialisable (for which you can also override the root XML namespace when serialised) or simply a string.

This same pattern is followed for arranging the expected output of the map, and for arranging the contents of flat files with the other map tester classes.

Assert - Compare the Actual Output Against the Expected Output

The MappingFromXmlToXmlTester class has the following method overrides for asserting the actual output from the map against the expected output that was arranged.

  • void AssertActualOutputXmlEqualsExpectedOutputXml()

  • AssertActualOutputXmlEqualsExpectedOutputXml(IXmlFilter xmlFilter) (see below)

All the comparison and logging and formatting of any differences is done for you. Easy!

Ignoring / Filtering XML Elements

The AssertActualOutputXmlEqualsExpectedOutputXml(IXmlFilter xmlFilter) override allows you to specify an IXmlFilter which allows you to specify a list of the local names of XML elements to ignore in comparisons, and/or a list of XPath expressions to ignore in comparisons.

Two constructor methods on XmlFilter allow you to easily specify these lists.

  • XmlFilter(IList<string> elementLocalNamesToIgnore)

  • XmlFilter(IList<string> elementLocalNamesToIgnore, IList<string> xpathsToIgnore)

Ignoring / Filtering Flat File Differences

Ignoring differences in flat files is a little more tricky, but not too difficult. The ChannelAdam library uses the DiffPlex library for performing text differences.

3 mechanisms on the flat file tester classes are provided for hooking into the DiffPaneModel difference result and manipulating it before a final decision is made as to whether or not the actual output is similar enough to the expected output.

  • An event that is raised with the differences
1    /// <summary>
2    /// Occurs when a text difference is detected, allowing a listener to filter the differences and
3    /// change the Line[x].Type to ChangeType.UnChanged so that the difference is no longer treated as a difference.
4    /// </summary>
5    /// <remarks>
6    /// This event or the TextDifferenceFilter property can be used for this purpose.
7    /// </remarks>
8    public event EventHandler<TextDifferenceDetectedEventArgs> TextDifferenceDetectedEvent;
  • An action delegate method that is called with the differences
1    /// <summary>
2    /// Gets or sets the Action delegate to be invoked when a text difference is detected, allowing differences to be filtered out by
3    /// changing the Line[x].Type to ChangeType.UnChanged - so that a difference is no longer treated as a difference.
4    /// </summary>
5    /// <value>The Action delegate.</value>
6    /// <remarks>
7    /// This property or TextDifferenceDetectedEvent can be used for this purpose.
8    /// </remarks>
9    public Action<DiffPaneModel> TextDifferenceFilter { get; set; }
  • A virtual method that allows you to inherit from the tester class and override the differences when they are detected.
1    protected virtual void OnTextDifferenceDetected(DiffPaneModel differences)

Modern Testing of BizTalk Maps

Overview

In today’s modern world of software development with continuous integration as a standard industry practice, what do you think would happen if you told a developer that they need to set a special Visual Studio project property in their build configuration in order for their code to “support unit testing”?

They would laugh at you in disbelief! Why does code need to be built in a special way in order to be testable?! This laughing typically would be followed by heart-ache, maybe a little quiet sobbing and an uncontrollable eye twitch as they realise that decade-old standard industry software development practices are crashing down all around them and they start reconsidering their life choices.

Sadly, the only thing that you can do is agree and try to console them by saying it is “just because it’s [technology xyz]".

This is the unfortunate world for a typical BizTalk developer using the out-of-the-box functionality.

And this embarrassing situation has other negative implications:

  • As described in Unit Testing with BizTalk Server Projects and Using the Unit Testing Feature with Schemas and Maps, BizTalk developers must set the Visual Studio project property “Enable Unit Testing” (under the Deployment tab) to “True”. This magically changes the base class of each BizTalk Map to “Microsoft.BizTalk.TestTools.Mapper.TestableMapBase” in order to “support unit testing”. Yes, an artefact’s base class is actually changed for that build – so what is being tested is not entirely the actual code that will be deployed into production. The difference between a Debug build and a Release build becomes more significant. (Admittedly for a BizTalk Map, this is less of a concern since the actual mapping is performed via XSLT under the covers and not directly by the base class, but the point is valid nevertheless – this approach is taken for other types of artefacts as well, and you really do not want to deploy Debug builds of any of those into a production environment…)

  • If you are like me and some teams I work with, you may want to develop and test the code off-premises (perhaps in your favourite cloud) and then deploy the release/deployment package to an on-premises test environment – where we also want to execute all the tests and have the test results recorded in the on-premises test and release management tools for all team members to access. However, a release/deployment package should never have that special project property set – and this prevents unit tests from compiling and executing against it.

  • When developing a BizTalk Map that uses a Scripting Functoid configured to execute a specified method of a class in an ‘External Assembly’, then “any assembly that you choose a script from … must also be in the global assembly cache (GAC) for Test Map to function correctly”. You need to GAC your external assemblies before running the unit tests. GAC —> Yuck!

Microsoft’s Integration Roadmap clearly shows a measured commitment to and place for BizTalk in the future, alongside their Azure Platform as a Service (PaaS) technologies. With the pending release of BizTalk 2016 as a drop-in replacement for BizTalk 2013 R2 (with no code recompilation required!), I thought it was time to refresh the practice of unit testing BizTalk Maps.

The Solution

The open source ChannelAdam Test Framework Library for BizTalk addresses all of these issues and provides the following benefits and features:

  • Works with BizTalk 2010 / 2013 / 2016
  • Never again set the project property “Enable Unit Testing” to “True”!
  • BizTalk Map unit tests can be executed against a release build
  • The provided map tester classes allow you to easily unit test both XML and flat file BizTalk Maps
  • The design of tester classes follow the clean Arrange-Act-Assert (AAA) testing pattern
  • Support for arranging the input and expected output from a string, XElement, an XML serialisable object or loading from an embedded resource
  • Performs schema validation of both the input and output by default (easily overridable)
  • Support for mocking External Assembly class methods
  • Support for filtering assertions of the expected output to allow for dynamic data (such as timestamps or GUIDs)
  • Detailed information is provided allowing you to see the differences between the expected output and the actual output of the map.

With this library you now have the tools needed to easily unit test you BizTalk Maps using modern testing practices. No excuses!

For information on how to get started, please see the usage documentation.

The ChannelAdam Test Framework Library for BizTalk

Overview

This is an open source .NET Framework 4.0, 4.5, 4.6 library providing functionality for the automated testing of BizTalk 2010/2013/2016 artifacts.

It provides you with an enhanced ability to use modern standard industry testing practices to test BizTalk Maps.

This library is compatible with the legacy, full .NET Framework 4.0 ChannelAdam Test Framework Libraries (version 1) only.

Please see the article Modern Testing of BizTalk Maps for background information on the issues with using Microsoft’s out-of-the-box functionality for testing Microsoft BizTalk Maps.

This library provides the following benefits and features:

  • Works with BizTalk 2010 / 2013 / 2016
  • Never again set the project property “Enable Unit Testing” to “True”!
  • BizTalk Map unit tests can be executed against a release build
  • The provided map tester classes allow you to easily unit test both XML and flat file BizTalk Maps
  • The design of tester classes follow the clean Arrange-Act-Assert (AAA) testing pattern
  • Support for arranging the input and expected output from a string, XElement, an XML serialisable object or loading from an embedded resource
  • Performs schema validation of both the input and output by default (easily overridable)
  • Support for mocking External Assembly class methods
  • Support for filtering assertions of the expected output to allow for dynamic data (such as timestamps or GUIDs)
  • Detailed information is provided allowing you to see the differences between the expected output and the actual output of the map.

The ChannelAdam Test Framework Libraries are fully compatible with MSTest (v1) and .NET Framework 4.0, 4.5, 4.6.

Note: I highly recommend using Behaviour-Driven Development practices and the usage of SpecFlow in conjunction with this library.

Getting Started

NuGet Package Installation

To install the ChannelAdam.TestFramework.BizTalk NuGet package run the following command in the Package Manager Console:

1PM> Install-Package ChannelAdam.TestFramework.BizTalk

Usage

The Basics

There are 4 map tester classes to be used depending on the type of input and output of your map:

  • BizTalkXmlToXmlMapTester - for testing a map that maps from XML to XML
  • BizTalkXmlToFlatFileMapTester - for testing a map that maps from XML to a flat file
  • BizTalkFlatFileToFlatFileMapTester - for testing a map that maps from a flat file to a flat file
  • BizTalkFlatFileToXmlMapTester - for testing a map that maps from a flat file to XML

Below is some sample MSTest test method code (in a very raw form) for testing a map that maps from XML to XML.

 1// ==================
 2// Variables
 3// ==================
 4
 5// Define some helpful variables for our testing artifacts
 6static Assembly thisAssembly = this.GetType().Assembly;
 7static string inputXmlTestDataEmbeddedResourceName          = thisAssembly.GetName().Name + ".InputXmlTestDataEmbeddedResource.xml"
 8static string expectedOutputXmlTestDataEmbeddedResourceName = thisAssembly.GetName().Name + ".ExpectedOutputXmlTestDataEmbeddedResource.xml"
 9
10// Create the MSTest compatible logging assertion class
11var logAssert = new ChannelAdam.TestFramework.MSTest.LogAssert();
12
13// Create the map tester - in this case for a map from XML to XML
14var xmlToXmlMapTester = new BizTalkXmlToXmlMapTester(logAssert);
15
16// ==================
17// Arrange
18// ==================
19
20// Arrange the input XML and expected output XML from embedded resources
21xmlToXmlMapTester.ArrangeInputXml(thisAssembly, inputXmlTestDataEmbeddedResourceName);
22xmlToXmlMapTester.ArrangeExpectedOutputXml(thisAssembly, expectedOutputXmlTestDataEmbeddedResourceName);
23
24// ==================
25// Act
26// ==================
27xmlToXmlMapTester.TestMap(new MySpiffyMapFromXmlToXml());
28
29// ==================
30// Assert
31// ==================
32xmlToXmlMapTester.AssertActualOutputXmlEqualsExpectedOutputXml();

Note: I highly recommend using the MoqTestFixture as a base class to your test class as it removes the need for you to be concerned with instantiating the LogAssert class and provdes a number of base methods to make testing easier. Please see the ChannelAdam Test Framework Libraries documentation for more information.

The test code above results in the following test output.

 110/10/16 10:00:00 PM - The input XML for the map is: 
 2<ns0:Request xmlns:ns0="http://SampleBizTalkMaps.Schemas.XmlRequest">
 3  <Key>Key_0</Key>
 4  <StringValue>StringValue_0</StringValue>
 5  <DateTimeValue>1999-05-31T13:20:00.000-05:00</DateTimeValue>
 6  <IntegerValue>100</IntegerValue>
 7  <DecimalValue>10.4</DecimalValue>
 8</ns0:Request>
 9
1010/10/16 10:00:00 PM - The expected output XML of the map is: 
11<ns0:Response xmlns:ns0="http://SampleBizTalkMaps.Schemas.XmlResponse">
12  <Key>Key_0</Key>
13  <StringValue>StringValue_0FAKE_GUID</StringValue>
14  <DateTimeValue>1999-05-31T13:20:00.000-05:00</DateTimeValue>
15  <IntegerValue>100</IntegerValue>
16  <DecimalValue>10.4</DecimalValue>
17</ns0:Response>
18
1910/10/16 10:00:00 PM - Validating the input XML for the BizTalk map
2010/10/16 10:00:00 PM - Executing the BizTalk map (XML to XML) MySpiffyMapFromXmlToXml
2110/10/16 10:00:00 PM - Asserting XML output from the BizTalk map exists is True
22
2310/10/16 10:00:00 PM - BizTalk map (XML to XML) execution completed
24
2510/10/16 10:00:00 PM - The actual output XML from the map is: 
26<ns0:Response xmlns:ns0="http://SampleBizTalkMaps.Schemas.XmlResponse">
27  <Key>Key_0</Key>
28  <StringValue>StringValue_0FAKE_GUID</StringValue>
29  <DateTimeValue>1999-05-31T13:20:00.000-05:00</DateTimeValue>
30  <IntegerValue>100</IntegerValue>
31  <DecimalValue>10.4</DecimalValue>
32</ns0:Response>
3310/10/16 10:00:00 PM - Validating the output XML from the BizTalk map
34
3510/10/16 10:00:00 PM - Asserting actual and expected XML are equal
3610/10/16 10:00:00 PM - Asserting The XML is as expected is True
3710/10/16 10:00:00 PM - The XML is as expected

And the test passed successfully!

Advanced Usage

More advanced functionality can be performed with the overloads on the methods used in the above test code.

Arrange - Input and Expected Output

The BizTalkXmlToXmlMapTester has the following method overrides for arranging the input XML.

  • void ArrangeInputXml(Assembly assembly, string resourceName) - to arrange the input XML from an embedded resource in the given assembly
  • void ArrangeInputXml(XElement xmlElement) - to arrange the input XML from a given XElement
  • void ArrangeInputXml(object valueToSerialise) - to arrange the input XML by serialising the given object
  • void ArrangeInputXml(object valueToSerialise, XmlRootAttribute xmlRootAttribute) - to arrange the input XML by serialising the given object and applying the given XmlRootAttribute during the serialisation
  • void ArrangeInputXml(object valueToSerialise, XmlAttributeOverrides xmlAttributeOverrides) - to arrange the input XML by serialising the given object and applying the given XmlAttributeOverrides during the serialisation
  • void ArrangeInputXml(string xmlValue) - to arrange the input XML from the given XML string

Input can be arranged from an embedded resource, XElement, object that is XML serialisable (for which you can also override the root XML namespace when serialised) or simply a string.

This same pattern is followed for arranging the expected output of the map, and for arranging the contents of flat files with the other map tester classes.

Act - Test the Map

The BizTalkXmlToXmlMapTester has the following method overrides for testing your BizTalk Map.

  • void TestMap(TransformBase map)
  • void TestMap(TransformBase map, IEnumerable<XsltExtensionObjectDescriptor> xsltExtensionObjectOverrides) (see below - Mocking External Assemblies)
  • void TestMap(TransformBase map, bool validateInputXml, bool validateOutputXml)
  • void TestMap(TransformBase map, IEnumerable<XsltExtensionObjectDescriptor> xsltExtensionObjectOverrides, bool validateInput, bool validateOutputXml)

The validateInputXml and validateOutputXml parameters are self-explanatory.

Mocking External Assemblies

The xsltExtensionObjectOverrides enumerable is the secret to mocking External Assembly class methods. To mock a class, create an Interface that define the method used in the map, and then create and set up a mock object from the Interface. Then use the following code to create the xsltExtensionObjectOverrides, populated with the mock object.

1// Mock the external assembly 'MyExternalAssemblyClassUsedInTheMap' class used in the map ;)
2var mocksOfExternalAssemblyClassesUsedInMap = new List<XsltExtensionObjectDescriptor>
3{
4    new XsltExtensionObjectDescriptor(typeof(MyExternalAssemblyClassUsedInTheMap), myMoqMockForThatClass.Object)
5};  

And test the map, providing the xsltExtensionObjectOverrides parameter.

1// Test the map which will execute methods on the mock objects
2xmlToXmlMapTester.TestMap(new MySpiffyMapFromXmlToXml(), mocksOfExternalAssemblyClassesUsedInMap);

For Other examples, including how to mock an External Assembly, please refer to this example BDD feature file and the implementation of the scenario steps.

Assert - Compare the Actual Output Against the Expected Output

The BizTalkXmlToXmlMapTester has the following method overrides for asserting the actual output from the map against the expected output that was arranged.

  • void AssertActualOutputXmlEqualsExpectedOutputXml()
  • AssertActualOutputXmlEqualsExpectedOutputXml(IXmlFilter xmlFilter)

All the comparison and logging and formatting of any differences is done for you. Easy!

Ignoring / Filtering XML Elements

The override allows you to specify an IXmlFilter which allows you to specify a list of the local names of XML elements to ignore in comparisons, and/or a list of XPath expressions to ignore in comparisons.

Two constructor methods on XmlFilter allow you to easily specify these lists.

  • XmlFilter(IList<string> elementLocalNamesToIgnore)
  • XmlFilter(IList<string> elementLocalNamesToIgnore, IList<string> xpathsToIgnore)

Ignoring / Filtering Flat File Differences

Ignoring differences in flat files is a little more tricky, but not too difficult. The ChannelAdam library uses the DiffPlex library for performing text differences.

3 mechanisms on the flat file tester classes are provided for hooking into the DiffPaneModel difference result and manipulating it before a final decision is made as to whether or not the actual output is similar enough to the expected output.

  • An event that is raised with the differences
1    /// <summary>
2    /// Occurs when a text difference is detected, allowing a listener to filter the differences and
3    /// change the Line[x].Type to ChangeType.UnChanged so that the difference is no longer treated as a difference.
4    /// </summary>
5    /// <remarks>
6    /// This event or the TextDifferenceFilter property can be used for this purpose.
7    /// </remarks>
8    public event EventHandler<TextDifferenceDetectedEventArgs> TextDifferenceDetectedEvent;
  • An action delegate method that is called with the differences
1    /// <summary>
2    /// Gets or sets the Action delegate to be invoked when a text difference is detected, allowing differences to be filtered out by
3    /// changing the Line[x].Type to ChangeType.UnChanged - so that a difference is no longer treated as a difference.
4    /// </summary>
5    /// <value>The Action delegate.</value>
6    /// <remarks>
7    /// This property or TextDifferenceDetectedEvent can be used for this purpose.
8    /// </remarks>
9    public Action<DiffPaneModel> TextDifferenceFilter { get; set; }
  • A virtual method that allows you to inherit from the tester class and override the differences when they are detected.
1    protected virtual void OnTextDifferenceDetected(DiffPaneModel differences)