WireMock-Net / WireMock.Net

WireMock.Net is a flexible product for stubbing and mocking web HTTP responses using advanced request matching and response templating. Based on the functionality from http://WireMock.org, but extended with more functionality.
Apache License 2.0
1.39k stars 207 forks source link

Is it possible to use WireMock as a middleware? #1035

Closed adrianiftode closed 4 days ago

adrianiftode commented 9 months ago

Currently I see that I can use wiremock hosted in different forms, however what I would like to do is to have a path prefix and everything after that path to be handled by wiremock.

So something like

app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWith("wiremock"))
    {
        await UseWiremock()
        return;
    }

    await next(context);
});

I would like to deploy an arbitrary .NET Service, but still use wiremock at the port 80 and let it handle some prefixed requests.

StefH commented 9 months ago

@adrianiftode Sounds like an interesting idea.

I guess you are using a modern .NET version like 6 or higher?

There is already an internal class named WireMockMiddleware which I register in the AspNetCoreSelfHost.cs:

appBuilder.UseMiddleware<WireMockMiddleware>();

Maybe that can be used?

adrianiftode commented 9 months ago

Yes, but the reason for asking this is I would like to deploy it with the tested service.

I have a service ServiceB that I would like to mock. Then I have the client service, ServiceA, that will do requests to ServiceB. ServiceB is an external (and expensive and legacy) service, and the only way to use it in test scenarios is to mock it.

I want to configure WireMock inside ServiceA. So when ServiceA is started, the Mocked ServiceB is also started and ready to accept requests. The Mock should listen to any HTTP request to a path that starts with /service-b-mock.

I will give it a try to the WireMockMiddleware.

To expand this, I would also like to deploy a WireMock image configured with different mocked services

StefH commented 9 months ago

A quick question:

Why not deploy 1 (or multiple) docker container(s) of WireMock.Net ?

adrianiftode commented 9 months ago

Even deploying it within a separated container, I was hoping to be a single one. I think I will go with this route anyway (multiple containers, one per every mocked service)

StefH commented 9 months ago

Or use one container and register the mappings with a prefix in the path?

matteus6007 commented 9 months ago

Why not just use wiremock as a docker container using docker compose, see https://github.com/matteus6007/MyDomain.Api.Template/blob/main/docker-compose.dev-env.yml#L58 as an example of setting this up, then add your mocks in the normal JSON format into the __files folder and change any paths in your config to http://localhost:8081/api-name/ where api-name is a unique name for each API you want to mock. Doing it like this means you don't have to add anything specific to your code.

matthewyost commented 6 months ago

I actually built this as a combination of a .NET HostedService (for Wiremock Server) and a DelegatingHandler which checks the original request headers for something like "X-WireMockStatus" and then rerouted all HttpClient calls to WireMockServer. This worked well to allow me to run this WireMockServer in all our lower environments for testing purposes.

StefH commented 2 months ago

@matteus6007 Is it possible that you share this complete solution?

matthewyost commented 2 months ago

@StefH Let me see what I can do about sharing this with you all.

Act0r commented 3 weeks ago

I'm very interested in that question as well. I'm working in enviroment where every service must have a bunch of specific middlewares, i can't avoid it. So i have to add that middlewares to wiremock or wiremock middleware to my service. So if this is possible please provide some hint in either direction

matthewyost commented 3 weeks ago

So here's a snapshot of how the implementation is performed:

WiremockServerInstance - This is the class that will be used by the background service.

    /// <summary>
    /// WireMockServer Instance object
    /// </summary>
    public class WireMockServerInstance
    {
        private readonly Action<WireMockServer> _configureAction;
        private readonly WireMockServerSettings _settings;

        #region Constructors

        /// <summary>
        /// Creates a new instance and provides ability to add configuration
        /// to the <see cref="WireMockServer"/>
        /// </summary>
        /// <param name="configure"></param>
        public WireMockServerInstance(Action<WireMockServer> configure)
            : this(configure, null) { }

        /// <summary>
        /// Creates a new instance and provides ability to add configuration
        /// for the start method of <see cref="WireMockServer"/>
        /// </summary>
        /// <param name="configure"></param>
        /// <param name="settings"></param>
        public WireMockServerInstance(Action<WireMockServer> configure, WireMockServerSettings settings)
        {
            _configureAction = configure;
            _settings = settings;
        }
        #endregion

        #region Properties

        /// <summary>
        /// Instance accessor for the <see cref="WireMockServer" />
        /// </summary>
        public WireMockServer Instance { get; private set; }

        /// <summary>
        /// Retrieves the URI for the <see cref="WireMockServer"/>
        /// </summary>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public string GetInstanceUri() => Instance.Urls.FirstOrDefault() ?? throw new Exception("No URL found for WireMockServer");

        #endregion

        #region Methods

        /// <summary>
        /// Configures and starts <see cref="WireMockServer"/> instance for use.
        /// </summary>
        public void Start()
        {
            Instance = (_settings != null)
                ? WireMockServer.Start(_settings)
                : WireMockServer.Start();

            _configureAction.Invoke(Instance);
        }

        #endregion

        /// <summary>
        /// Stops the <see cref="WireMockServer"/>
        /// </summary>
        public void Stop()
        {
            if (Instance != null && (Instance.IsStarted || Instance.IsStartedWithAdminInterface))
                Instance.Stop();
        }
    }

WiremockContext - Context to allow me to control certain functionality of the Wiremock instance

    /// <summary>
    /// Wiremock context
    /// </summary>
    public class WiremockContext : IWiremockContext
    {
        /// <summary>
        /// Is Wiremock enabled?
        /// </summary>
        public bool? IsEnabled { get; set; }

        /// <summary>
        /// Duration to delay the response in milliseconds
        /// </summary>
        public int ResponseDelayInMs { get; set; }
    }

WireMockDelegationHandler - DelegatingHandler class allowing us to tap into the HttpClient object and perform our magic redirects to Wiremock without having to change our code. THIS is where the magic happens

    /// <summary>
    /// DelegatingHandler that takes requests made via the <see cref="HttpClient"/>
    /// and routes them to the <see cref="WireMockServer"/>
    /// </summary>
    public class WireMockDelegationHandler : DelegatingHandler
    {
        private readonly WireMockServerInstance _server;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly ILogger<WireMockDelegationHandler> _logger;

        /// <summary>
        /// Creates a new instance of <see cref="WireMockDelegationHandler"/>
        /// </summary>
        /// <param name="server"></param>
        /// <param name="httpContextAccessor"></param>
        /// <param name="logger"></param>
        /// <exception cref="ArgumentNullException"></exception>
        public WireMockDelegationHandler(WireMockServerInstance server, IHttpContextAccessor httpContextAccessor, ILogger<WireMockDelegationHandler> logger)
        {
            _server = server ?? throw new ArgumentNullException(nameof(server));
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
            _logger = logger;
        }

        /// <inheritdoc />
        /// <exception cref="ArgumentNullException"></exception>
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (request is null)
                throw new ArgumentNullException(nameof(request));

            if (_httpContextAccessor.HttpContext is null)
                throw new ArgumentNullException(nameof(_httpContextAccessor.HttpContext));

            bool shouldRedirectToWireMock = IsWireMockStatusHeaderSet();

            var (shouldDelayResponse, delayInMs) = IsDelayHeaderSet();

            if (shouldRedirectToWireMock)
            {
                _logger?.LogDebug("Redirecting request to WireMock server");
                if (_server.Instance is not null
                    && _server.Instance.Urls is not null
                    && _server.Instance.Urls.Any())
                    request.RequestUri = new Uri(_server.GetInstanceUri() + request.RequestUri.PathAndQuery);
            }

            if (shouldDelayResponse)
                await Task.Delay(delayInMs);

            return await base.SendAsync(request, cancellationToken);
        }

        private bool IsWireMockStatusHeaderSet()
        {
            bool shouldRedirectToWireMock = false;
            if (_httpContextAccessor.HttpContext.Request.Headers.ContainsKey(AppConstants.HEADER_WIREMOCK_STATUS))
            {
                _logger?.LogDebug("Found WireMock header on request");

                if (_httpContextAccessor.HttpContext.Request.Headers[AppConstants.HEADER_WIREMOCK_STATUS].ToString().Equals("true", StringComparison.OrdinalIgnoreCase))
                    shouldRedirectToWireMock = true;
            }
            return shouldRedirectToWireMock;
        }

        private (bool, int) IsDelayHeaderSet()
        {
            bool shouldDelayResponse = false;
            int delayInMs = 0;

            if (_httpContextAccessor.HttpContext.Request.Headers.ContainsKey(AppConstants.HEADER_RESPONSE_DELAY))
            {
                string delay = _httpContextAccessor.HttpContext.Request.Headers[AppConstants.HEADER_RESPONSE_DELAY].ToString();
                if (!int.TryParse(delay, out delayInMs))
                    throw new ArgumentOutOfRangeException(nameof(delay), "Delay must be an integer");

                _logger?.LogDebug("Delaying response by {0}ms", delayInMs);
                shouldDelayResponse = true;
            }

            return (shouldDelayResponse, delayInMs);
        }
    }

WireMockBgService - This is the background service that will hold onto our instance of Wiremock and allow us to keep from spinning up new copies with every request.

    /// <summary>
    /// <see cref="BackgroundService"/> used to start/stop the <see cref="WireMockServer"/>
    /// </summary>
    public class WireMockBgService : BackgroundService
    {
        private readonly WireMockServerInstance _server;

        /// <summary>
        /// Creates a new <see cref="BackgroundService"/> using an instance
        /// of <see cref="WireMockServerInstance"/>
        /// </summary>
        /// <param name="server"></param>
        public WireMockBgService(WireMockServerInstance server)
        {
            _server = server ?? throw new ArgumentNullException(nameof(server));
        }

        /// <inheritdoc />
        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _server.Start();
            return Task.CompletedTask;
        }

        /// <inheritdoc />
        public override Task StopAsync(CancellationToken cancellationToken)
        {
            _server.Stop();
            return base.StopAsync(cancellationToken);
        }
    }

ServiceCollectionExtensions - Extension methods to make it easy for integrating into any app.

    /// <summary>
    /// Extension methods for <see cref="IServiceCollection"/>.
    /// </summary>
    public static class ServiceCollectionExtensions
    {
        /// <summary>
        /// Adds all the components necessary to run Wiremock.NET in the background.
        /// </summary>
        /// <param name="services"></param>
        /// <param name="server"></param>
        /// <returns></returns>
        public static IServiceCollection AddWireMockService(this IServiceCollection services, Action<WireMockServer> server)
        {
            return services.AddWireMockService(server, null);
        }

        /// <summary>
        /// Adds all the components necessary to run Wiremock.NET in the background.
        /// </summary>
        /// <param name="services"></param>
        /// <param name="configure"></param>
        /// <param name="settings"></param>
        /// <returns></returns>
        public static IServiceCollection AddWireMockService(this IServiceCollection services, Action<WireMockServer> configure, WireMockServerSettings settings)
        {
            services.AddTransient<WireMockDelegationHandler>();

            if (settings is null)
                services.AddSingleton(new WireMockServerInstance(configure));
            else
                services.AddSingleton(new WireMockServerInstance(configure, settings));

            services.AddHostedService<WireMockBgService>();
            services.AddHttpClient();
            services.AddHttpContextAccessor();
            services.ConfigureAll<HttpClientFactoryOptions>(options =>
            {
                options.HttpMessageHandlerBuilderActions.Add(builder =>
                {
                    builder.AdditionalHandlers.Add(builder.Services.GetRequiredService<WireMockDelegationHandler>());
                });
            });
            return services;
        }
    }

Now for it's usage! Using a minimal API, we can have something like this:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();

if (!builder.Environment.IsProduction())
{
    builder.Services.AddWireMockService(server =>
    {
            server.Given(Request.Create()
                .WithPath("<your path that you want to mock>")
                .UsingAnyMethod()
            ).RespondWith(Response.Create()
                .WithStatusCode(200)
                .WithBody("<your body to respond with when it's called>");
    });
}

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseAuthorization();

app.MapControllers();

app.Run();

Hopefully this helps some of y'all develop the pattern to make this a thing!

Act0r commented 2 weeks ago

Thank you very much for the detailed example

StefH commented 1 week ago

@matthewyost I did try your solution, however it seems that the path defined in WithPath is not available, it returns a 404.

matthewyost commented 1 week ago

@StefH The stuff between the < > is just supposed to be whatever path you want to use. It's meant to be replaced with whatever path you're attempting to mock a response for.

StefH commented 1 week ago

@StefH The stuff between the < > is just supposed to be whatever path you want to use. It's meant to be replaced with whatever path you're attempting to mock a response for.

About the "path" : I did change that. If you have time, please review my PR: https://github.com/WireMock-Net/WireMock.Net/pull/1175

StefH commented 1 week ago

@Act0r did you get it working?

Act0r commented 6 days ago

@StefH Actually i posponed the idea and decide to implement requested features by myself.

StefH commented 6 days ago

@Act0r @matteus6007 @matthewyost

I got it working. I was thinking in the wrong direction...

This solution actually translates any calls made from a WebApplication to another API (e.g. https://real-api:12345/test1) to a call to the running WireMock.Net instance (as background service) in the same WebApplication. This is done by changing the behavior from the HttpClient used in that WebApplication.

Example:

app.MapGet("/weatherforecast", async (HttpClient client) =>
{
    // ⭐ This injected HttpClient will not call the real api, but will call WireMock.Net !
    var result = await client.GetStringAsync("https://real-api:12345/test1");

    return Enumerable.Range(1, 3).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            result
        ));
});

I'm not sure that is the same idea as @adrianiftode had?

My thoughts were initially that this WireMock.Net instance would be handling additional calls in a WebApplication.

StefH commented 4 days ago

PR is merged.