如何集成测试 SoapCore 端点?

问题描述 投票:0回答:3

我在 .net core 3.1 应用程序中使用 SoapCore,并且我已经为使用 Web 应用程序工厂的控制器设置了集成测试策略,例如

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

            return client;
        }
    }

这适用于我的控制器集成测试,如下所示:

[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");
}

但我现在正在尝试测试一些使用 SoapCore 运行的肥皂端点。这是在启动文件中:

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);
    });
}

wsdl 文件是预先生成的。

然后在我的测试项目中,我通过浏览到 wsdl 文件添加了一个连接的服务,并编写了这个测试主体:

    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();

我得到这个例外:

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.'

没有内在的异常。

有趣的是,通过使用我的控制器测试使用的相同客户端,这给了我 200:

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

查看连接服务 > MyService > 参考,我可以看到有一个基于参考的端口,这有点令人担忧,但我相信我在测试主体中指定的新端点应该意味着不会被使用。

    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));
    }
asp.net-core soap integration-testing soapcore
3个回答
3
投票

基于 @Craig (OP) 回答的代码进行更新,但适用于您没有生成的 WCF 客户端的情况。本答案中进一步提供了具有完整设置的

IMySoapSvc
代码。

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);
        }
    }
}

使用 SOAP 客户端的替代方法是使用常规 POST 请求。

下面是支持 SOAP 1.1 和 1.2 的简单 Hello World SOAP 服务的分步说明。最后,有一些使用

WebApplicationFactory
的测试,然后使用
ChannelFactory

添加此 Nuget(或更新的版本(如果可用))

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

SOAP 服务

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";
        }
    }
}

启动#配置服务

using var iisUrlRewriteStreamReader = File.OpenText("RewriteRules.xml");
var options = new RewriteOptions()
   .AddIISUrlRewrite(iisUrlRewriteStreamReader);
app.UseRewriter(options);

services.AddSingleton<IMySoapSvc, MySoapSvc>();

启动#配置

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);

重写规则以在 SOAP 1.1/1.2 之间进行划分。将其放入与 Startup.cs 相同的文件夹中的文件 RewriteRules.xml 中。

<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>

您将需要在项目文件中使用它来实现 RewriteRules.xml

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

最后是测试。在这里我们可以看到 SOAP 1.1 和 SOAP 1.2 请求之间的详细差异。

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());
        }
    }
}

另一种方法是使用

ChannelFactory
获取在端口 5000 上运行的普通主机的客户端。为此,我们需要在测试基类中启动 Web 环境。这种方法的运行速度明显快于
WebApplicationFactory
。请参阅本答案末尾的屏幕截图。

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>();
                });
    }
}

然后是测试课。这是非常标准的代码。

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);
        }
    }
}

显示测试所用时间的屏幕截图。获胜者是使用在端口 5000 上运行的主机的

WcfTest
,第二名是使用
BasicTests
和普通 POST 请求的
WebApplicationFactory

enter image description here

更新:由于设置时间更短,使用 NUnit 和

WebApplicationFactory
(此处未显示)的测试运行速度快了 4 倍。

使用 .NET Core 5 进行测试。


2
投票

我通过使用通过问题中发现的 WebApplicationFactory 创建的客户端解决了这个问题 - 因为集成测试在内存中运行,并且 WCF 客户端根据 fiddler 创建真实的网络请求,这显然不起作用(没有服务器接受实际网络)请求),以及从这些 github 问题中提取并稍微修改的代码:

https://github.com/dotnet/wcf/issues/2400

https://github.com/dotnet/wcf/issues/4214

我添加了 3 个新课程:

新行为

internal class HttpMessageHandlerBehavior : IEndpointBehavior
{
    private readonly HttpClient _client;

    public HttpMessageHandlerBehavior(HttpClient _client)
    {
        this._client = _client;
    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        bindingParameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(GetHttpMessageHandler));
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }

    public void Validate(ServiceEndpoint endpoint) { }

    public HttpMessageHandler GetHttpMessageHandler(HttpClientHandler httpClientHandler)
    {
        return new InterceptingHttpMessageHandler(httpClientHandler, _client);
    }

    public Func<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> OnSendingAsync { get; set; }
    public Func<HttpResponseMessage, CancellationToken, Task<HttpResponseMessage>> OnSentAsync { get; set; }
}

拦截上述类使用的请求的处理程序

internal class InterceptingHttpMessageHandler : DelegatingHandler
{
    private readonly HttpClient _client;

    public InterceptingHttpMessageHandler(HttpMessageHandler innerHandler, HttpClient client)
    {
        InnerHandler = innerHandler;
        _client = client;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await _client.PostAsync(request.RequestUri, request.Content, cancellationToken);
    }
}

还有一个扩展方法来抽象这一切(注意上面的内部关键字)

public static class ServiceEndpointExtensions {

    public static void InterceptRequestsWithHttpClient(this ServiceEndpoint endpoint, HttpClient httpClient)
    {
        endpoint.EndpointBehaviors.Add(new HttpMessageHandlerBehavior(httpClient));
    }
}

用途:

    private readonly DownloadWebServiceClient _sut;

    public DownloadWebServiceTests()
    {
        var endpoint = new EndpointAddress(TestConstants.Root + "/service/download.svc");
        var binding = new BasicHttpBinding(BasicHttpSecurityMode.None);
        var client = new DownloadWebServiceClient(binding, endpoint);
        client.Endpoint.InterceptRequestsWithHttpClient(_client);

        _sut = client;
    }

    [Fact]
    public async Task GetDocument_Unauthenticated_Throws()
    {
        var request = new DownloadRequest
        {
            Authentication = new Authentication
            {
                Username = "hello",
                Password = "good bye"
            },
            DocumentId = 1
        };

        _sut.Invoking(async s => await s.GetDocumentAsync(request))
            .Should()
            .Throw<FaultException>();
    }

0
投票

我不知道为什么,但我需要将@Craig

SendAsync
方法从
InterceptingHttpMessageHandler
更改为:

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var _request = new HttpRequestMessage(HttpMethod.Post, request.RequestUri)
    {
        Content = request.Content!
    };

    foreach (var header in request.Headers)
    {
        _request.Headers.TryAddWithoutValidation(header.Key, header.Value);
    }

    return await _client.SendAsync(_request, cancellationToken);
}

我收到以下错误:

带有 Action '' 的消息无法在接收方处理,因为 EndpointDispatcher 处的 ContractFilter 不匹配...

© www.soinside.com 2019 - 2024. All rights reserved.