asp.net-coresoapintegration-testingsoapcore

How do I integration test a SoapCore endpoint?


I am using SoapCore in a .net core 3.1 app and I have setup an integration testing strategy for controllers that uses a web application factory eg

internal class IntegrationTestApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
        where TStartup : class
    {
        public HttpClient CreateConfiguredHttpClient()
        {
            var client = this.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });

            return client;
        }
    }

This works for my controller integration tests like so:

[Fact]
public async Task OrderDelivery_NoRequestBody_ReturnsBadRequest()
{
    var xml = ...
    var response = await _client
            .PostAsync("http://localhost/api/myController", new StringContent(xml));;
    var responseBody = await response.Content.ReadAsStringAsync();

    response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    responseBody.Should().Contain("Invalid XML");
}

But I am now trying to test some soap endpoints that are working using SoapCore. This is in the startup file:

public static IApplicationBuilder AddSoapCoreEndpoints(this IApplicationBuilder app, IConfiguration 
    config, IHostEnvironment env)
{
    var settings = config.GetSection("FileWSDL").Get<WsdlFileOptions>();
    settings.AppPath = env.ContentRootPath;

    app.Map("/service", x =>
    {
        x.UseSoapEndpoint<IService>("/MyService.svc", new BasicHttpBinding(), SoapSerializer.XmlSerializer, false, null, settings);
    });
}

The wsdl files are pre-generated.

So then in back in my test project I have added a connected service by browsing to the wsdl file, and written this test body:

    var endpoint = new EndpointAddress("http://localhost/service/MyService.svc");
    var binding = new BasicHttpBinding(BasicHttpSecurityMode.None);
    var client = new MyServiceClient(binding, endpoint);
    var response = await client.DoSomething();

I get this exception:

System.ServiceModel.EndpointNotFoundException: 'There was no endpoint listening at http://localhost/service/MyService.svc that could accept the message. This is often caused by an incorrect address or SOAP action. See InnerException, if present, for more details.'

There is no inner exception.

Interestingly though this gives me a 200 by using the same client my controller tests use:

await _client.GetAsync("http://localhost/service/MyService.svc");

Looking at Connected Services > MyService > Reference I can see that there is a port based into the reference, which is a little concerning, but I believe the new endpoint I have specified in the test body should mean that wont be used.

    private static System.ServiceModel.EndpointAddress GetEndpointAddress(EndpointConfiguration endpointConfiguration)
    {
        if ((endpointConfiguration == EndpointConfiguration.BasicHttpBinding_IMyService))
        {
            return new System.ServiceModel.EndpointAddress("https://localhost:44399/service/MyService.svc");
        }
        throw new System.InvalidOperationException(string.Format("Could not find endpoint with name \'{0}\'.", endpointConfiguration));
    }

Solution

  • An update based on code in answer from @Craig (OP), but for cases where you don't have a generated WCF-client. Code for IMySoapSvc with a complete setup is provided further down in this answer.

    using System;
    using System.ServiceModel;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc.Testing;
    using MyMicroservice;
    using MyMicroservice.SoapSvc;
    using Xunit;
    
    namespace XUnitTestProject.Craig
    {
        public class WcfWebApplicationFactoryTest : IClassFixture<WebApplicationFactory<Startup>>
        {
            private readonly WebApplicationFactory<Startup> _factory;
    
            public WcfTest(WebApplicationFactory<Startup> factory)
            {
                _factory = factory;
            }
    
            [Fact]
            public async Task sayHello_normalCond_receive_HelloWorld()
            {
                await Task.Delay(1); // because of some issues with WebApplicationFactory, test method needs to be async
    
                var endpoint = new EndpointAddress(new Uri("http://localhost/MyService.svc"));
                var binding = new BasicHttpBinding(BasicHttpSecurityMode.None);
                using var channelFactory = new ChannelFactory<IMySoapSvc>(binding, endpoint);
    
                // entry point for code from @Craig
                channelFactory.Endpoint.InterceptRequestsWithHttpClient(_factory.CreateClient());
    
                var wcfClient = channelFactory.CreateChannel();
                var response = wcfClient.SayHello();
    
                Assert.Equal("Hello world", response);
            }
        }
    }
    

    An alternative to using a SOAP-client, can be to use regular POST requests.

    Below is a step-by-step for a simple Hello World SOAP-service with support for both SOAP 1.1 and 1.2. At the end, there are a couple of tests using WebApplicationFactory, and then using ChannelFactory.

    Add this Nuget (or newer when available)

    <PackageReference Include="SoapCore" Version="1.1.0.7" />
    

    The SOAP service

    using System.ServiceModel;
    
    namespace MyMicroservice.SoapSvc
    {
        [ServiceContract(Name = "MySoapSvc", Namespace = "http://www.mycompany.no/mysoap/")]
        public interface IMySoapSvc
        {
            [OperationContract(Name = "sayHello")]
            string SayHello();
        }
    
        public class MySoapSvc : IMySoapSvc
        {
            public string SayHello()
            {
                return "Hello world";
            }
        }
    }
    

    Startup#ConfigureServices

    using var iisUrlRewriteStreamReader = File.OpenText("RewriteRules.xml");
    var options = new RewriteOptions()
       .AddIISUrlRewrite(iisUrlRewriteStreamReader);
    app.UseRewriter(options);
    
    services.AddSingleton<IMySoapSvc, MySoapSvc>();
    

    Startup#Configure

    var soap12Binding = new CustomBinding(new TextMessageEncodingBindingElement(MessageVersion.Soap12WSAddressingAugust2004, System.Text.Encoding.UTF8),
                    new HttpTransportBindingElement());
    
    app.UseSoapEndpoint<IMySoapSvc>("/MyService.svc", new BasicHttpBinding(), SoapSerializer.XmlSerializer);
                
    app.UseSoapEndpoint<IMySoapSvc>("/MyService12.svc", soap12Binding, SoapSerializer.XmlSerializer);
    

    Rewrite rules to split between SOAP 1.1/1.2. Put this in file RewriteRules.xml in same folder as Startup.cs.

    <rewrite>
      <rules>
        <rule name="Soap12" stopProcessing="true">
          <match url="(.*)\.svc" />
          <conditions>
            <add input="{REQUEST_METHOD}" pattern="^POST$" />
            <add input="{CONTENT_TYPE}" pattern=".*application/soap\+xml.*" />
          </conditions>
          <action type="Rewrite" url="/{R:1}12.svc" appendQueryString="false" />
        </rule>
      </rules>
    </rewrite>
    

    You'll need this in your project file for RewriteRules.xml

    <ItemGroup>
        <None Update="RewriteRules.xml">
          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
        </None>
    </ItemGroup>
    

    And finally the tests. Here we can see the differences in detail between SOAP 1.1 and SOAP 1.2 requests.

    using System.Net;
    using System.Net.Http;
    using System.Text;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc.Testing;
    using MyMicroservice;
    using Xunit;
    
    namespace XUnitTestProject
    {
        public class BasicTests : IClassFixture<WebApplicationFactory<Startup>>
        {
            private readonly WebApplicationFactory<Startup> _factory;
    
            public BasicTests(WebApplicationFactory<Startup> factory)
            {
                _factory = factory;
            }
    
            [Theory]
            [InlineData("/MyService.svc")]
            public async Task helloWorld_validEnvelope11_receiveOk(string url) {
                const string envelope = @"<?xml version=""1.0"" encoding=""utf-8""?>
                  <soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
                                 xmlns:xsd=""http://www.w3.org/2001/XMLSchema""
                                 xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
                  <soap:Body>
                    <sayHello xmlns=""http://www.mycompany.no/mysoap/""></sayHello>
                  </soap:Body>
                </soap:Envelope>";
    
                var client = _factory.CreateClient();
                client.DefaultRequestHeaders.Add("SOAPAction", "http://localhost/mysoap/sayHello");
    
                var response = await client
                    .PostAsync(url, new StringContent(envelope, Encoding.UTF8, "text/xml"));
    
                Assert.Equal(HttpStatusCode.OK, response.StatusCode);
                Assert.Contains("Hello world", await response.Content.ReadAsStringAsync());
            }
    
            [Theory]
            [InlineData("/MyService.svc")]
            public async Task helloWorld_validEnvelope12_receiveOk(string url)
            {
                const string envelope = @"<?xml version=""1.0"" encoding=""utf-8""?>
                    <soap12:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
                                     xmlns:xsd=""http://www.w3.org/2001/XMLSchema""
                                     xmlns:soap12=""http://www.w3.org/2003/05/soap-envelope"">
                    <soap12:Body >
                        <sayHello xmlns=""http://www.mycompany.no/mysoap/""></sayHello>
                    </soap12:Body>
                </soap12:Envelope>";
    
                var client = _factory.CreateClient();
    
                var response = await client
                    .PostAsync(url, new StringContent(envelope, Encoding.UTF8, "application/soap+xml"));
    
                Assert.Equal(HttpStatusCode.OK, response.StatusCode);
                Assert.Contains("Hello world", await response.Content.ReadAsStringAsync());
            }
        }
    }
    

    Another approach is to use ChannelFactory for getting a client with a plain host running on port 5000. For this, we'll need to bring up the web environment in a test base class. This approach runs significantly faster than WebApplicationFactory. See screenshot at the end of this answer.

    using System;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.Hosting;
    using MyMicroservice;
    using Xunit;
    
    namespace XUnitTestProject
    {
        public class HostFixture : IAsyncLifetime
        {
            private IHost _host;
    
            public async Task InitializeAsync()
            {
                _host = CreateHostBuilder().Build();
                await _host.StartAsync();
            }
    
            public async Task DisposeAsync()
            {
                await _host.StopAsync();
                _host.Dispose();
            }
    
            private static IHostBuilder CreateHostBuilder() =>
                Host.CreateDefaultBuilder(Array.Empty<string>())
                    .ConfigureWebHostDefaults(webBuilder =>
                    {
                        webBuilder.UseStartup<Startup>();
                    });
        }
    }
    

    And then the test class. This is pretty standard code.

    using System;
    using System.ServiceModel;
    using System.ServiceModel.Channels;
    using MyMicroservice.SoapSvc;
    using Xunit;
    
    namespace XUnitTestProject
    {
        public class WcfTest : IClassFixture<HostFixture>
        {
            [Theory]
            [InlineData("http://localhost:5000/MyService.svc")]
            public void sayHello_normalCond_receiveHelloWorld11(string url)
            {
                var binding = new BasicHttpBinding();
                var endpoint = new EndpointAddress(new Uri(url));
                using var channelFactory = new ChannelFactory<IMySoapSvc>(binding, endpoint);
    
                var serviceClient = channelFactory.CreateChannel();
                var response = serviceClient.SayHello();
                Assert.Equal("Hello world", response);
            }
    
            [Theory]
            [InlineData("http://localhost:5000/MyService.svc")]
            public void sayHello_normalCond_receiveHelloWorld12(string url)
            {
                var soap12Binding = new CustomBinding(
                    new TextMessageEncodingBindingElement(MessageVersion.Soap12WSAddressingAugust2004, System.Text.Encoding.UTF8),
                    new HttpTransportBindingElement());
    
                var endpoint = new EndpointAddress(new Uri(url));
                using var channelFactory = new ChannelFactory<IMySoapSvc>(soap12Binding, endpoint);
    
                var serviceClient = channelFactory.CreateChannel();
                var response = serviceClient.SayHello();
                Assert.Equal("Hello world", response);
            }
        }
    }
    

    Screenshot showing elapsed time for tests. The winner is WcfTest using host running on port 5000, in second place is BasicTests using WebApplicationFactory and plain POST requests.

    enter image description here

    Update: Tests with NUnit and WebApplicationFactory (not shown here) runs in a magnitude of 4 times faster due to shorter setup time.

    Tested using .NET Core 5.