diff --git a/README.md b/README.md
index 7840caef..4108dac3 100644
--- a/README.md
+++ b/README.md
@@ -68,13 +68,15 @@ WireMock.Net can be used in several ways:
You can use your favorite test framework and use WireMock within your tests, see
[Wiki : UnitTesting](https://github.com/StefH/WireMock.Net/wiki/Using-WireMock-in-UnitTests).
+### Unit/Integration Testing using Testcontainers.DotNet
+You can use [Wiki : WireMock.Net.Testcontainers](https://github.com/WireMock-Net/WireMock.Net/wiki/Using-WireMock.Net.Testcontainers) to build a WireMock.Net Docker container which can be used in Unit/Integration testing.
+
### As a dotnet tool
It's simple to install WireMock.Net as (global) dotnet tool, see [Wiki : dotnet tool](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-dotnet-tool).
### As standalone process / console application
This is quite straight forward to launch a mock server within a console application, see [Wiki : Standalone Process](https://github.com/StefH/WireMock.Net/wiki/WireMock-as-a-standalone-process).
-
### As a Windows Service
You can also run WireMock.Net as a Windows Service, follow this [WireMock-as-a-Windows-Service](https://github.com/WireMock-Net/WireMock.Net/wiki/WireMock-as-a-Windows-Service).
diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln
index 6e07ee6c..9e767796 100644
--- a/WireMock.Net Solution.sln
+++ b/WireMock.Net Solution.sln
@@ -111,6 +111,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMockAzureQueueExample",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WireMockAzureQueueProxy", "examples\WireMockAzureQueueProxy\WireMockAzureQueueProxy.csproj", "{ADB557D8-D66B-4387-912B-3F73E290B478}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Testcontainers", "src\WireMock.Net.Testcontainers\WireMock.Net.Testcontainers.csproj", "{12B016A5-9D8B-4EFE-96C2-CA51BE43367D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.TestcontainersExample", "examples\WireMock.Net.TestcontainersExample\WireMock.Net.TestcontainersExample.csproj", "{56A38798-C48B-4A4A-B805-071E05C02CE1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -265,6 +269,14 @@ Global
{ADB557D8-D66B-4387-912B-3F73E290B478}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ADB557D8-D66B-4387-912B-3F73E290B478}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ADB557D8-D66B-4387-912B-3F73E290B478}.Release|Any CPU.Build.0 = Release|Any CPU
+ {12B016A5-9D8B-4EFE-96C2-CA51BE43367D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {12B016A5-9D8B-4EFE-96C2-CA51BE43367D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {12B016A5-9D8B-4EFE-96C2-CA51BE43367D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {12B016A5-9D8B-4EFE-96C2-CA51BE43367D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {56A38798-C48B-4A4A-B805-071E05C02CE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {56A38798-C48B-4A4A-B805-071E05C02CE1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {56A38798-C48B-4A4A-B805-071E05C02CE1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {56A38798-C48B-4A4A-B805-071E05C02CE1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -309,6 +321,8 @@ Global
{7C2A9DE8-C89F-4841-9058-6B9BF81E5E34} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{BAA9EC2A-874B-45CE-8E51-A73622DC7F3D} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
{ADB557D8-D66B-4387-912B-3F73E290B478} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
+ {12B016A5-9D8B-4EFE-96C2-CA51BE43367D} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2}
+ {56A38798-C48B-4A4A-B805-071E05C02CE1} = {985E0ADB-D4B4-473A-AA40-567E279B7946}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DC539027-9852-430C-B19F-FD035D018458}
diff --git a/WireMock.Net Solution.sln.DotSettings b/WireMock.Net Solution.sln.DotSettings
index 6f5a8ca1..c3564a0f 100644
--- a/WireMock.Net Solution.sln.DotSettings
+++ b/WireMock.Net Solution.sln.DotSettings
@@ -10,6 +10,7 @@
IP
MD5
OPTIONS
+ OS
PATCH
POST
PUT
@@ -32,6 +33,7 @@
True
True
True
+ True
True
True
True
diff --git a/examples/WireMock.Net.TestcontainersExample/Program.cs b/examples/WireMock.Net.TestcontainersExample/Program.cs
new file mode 100644
index 00000000..bf26c03d
--- /dev/null
+++ b/examples/WireMock.Net.TestcontainersExample/Program.cs
@@ -0,0 +1,43 @@
+using Newtonsoft.Json;
+using Testcontainers.MsSql;
+using WireMock.Net.Testcontainers;
+
+namespace WireMock.Net.TestcontainersExample;
+
+internal class Program
+{
+ private static async Task Main(string[] args)
+ {
+ var container = new WireMockContainerBuilder()
+ .WithAdminUserNameAndPassword("x", "y")
+ .WithMappings(@"C:\Dev\GitHub\WireMock.Net\examples\WireMock.Net.Console.NET6\__admin\mappings")
+ .WithWatchStaticMappings(true)
+ .WithAutoRemove(true)
+ .WithCleanUp(true)
+ .Build();
+
+ await container.StartAsync().ConfigureAwait(false);
+
+ var logs = await container.GetLogsAsync(DateTime.Now.AddDays(-1)).ConfigureAwait(false);
+ Console.WriteLine("logs = " + logs.Stdout);
+
+ var restEaseApiClient = container.CreateWireMockAdminClient();
+
+ var settings = await restEaseApiClient.GetSettingsAsync();
+ Console.WriteLine("settings = " + JsonConvert.SerializeObject(settings, Formatting.Indented));
+
+ var mappings = await restEaseApiClient.GetMappingsAsync();
+ Console.WriteLine("mappings = " + JsonConvert.SerializeObject(mappings, Formatting.Indented));
+
+ var client = container.CreateClient();
+ var result = await client.GetStringAsync("/static/mapping");
+ Console.WriteLine("result = " + result);
+
+ await container.StopAsync();
+
+ var sql = new MsSqlBuilder()
+ .WithAutoRemove(true)
+ .WithCleanUp(true)
+ .Build();
+ }
+}
\ No newline at end of file
diff --git a/examples/WireMock.Net.TestcontainersExample/WireMock.Net.TestcontainersExample.csproj b/examples/WireMock.Net.TestcontainersExample/WireMock.Net.TestcontainersExample.csproj
new file mode 100644
index 00000000..14bf542e
--- /dev/null
+++ b/examples/WireMock.Net.TestcontainersExample/WireMock.Net.TestcontainersExample.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/WireMock.Net.Testcontainers/Models/ContainerInfo.cs b/src/WireMock.Net.Testcontainers/Models/ContainerInfo.cs
new file mode 100644
index 00000000..f3011069
--- /dev/null
+++ b/src/WireMock.Net.Testcontainers/Models/ContainerInfo.cs
@@ -0,0 +1,7 @@
+namespace WireMock.Net.Testcontainers.Models;
+
+internal record ContainerInfo
+(
+ string Image,
+ string MappingsPath
+);
\ No newline at end of file
diff --git a/src/WireMock.Net.Testcontainers/WireMock.Net.Testcontainers.csproj b/src/WireMock.Net.Testcontainers/WireMock.Net.Testcontainers.csproj
new file mode 100644
index 00000000..8c3dad81
--- /dev/null
+++ b/src/WireMock.Net.Testcontainers/WireMock.Net.Testcontainers.csproj
@@ -0,0 +1,36 @@
+
+
+
+ A fluent testcontainer builder for the Docker version of WireMock.Net
+ netstandard2.0;netstandard2.1
+ true
+ wiremock;docker;testcontainer;testcontainers
+ {12B016A5-9D8B-4EFE-96C2-CA51BE43367D}
+ true
+ ../WireMock.Net/WireMock.Net.ruleset
+ true
+ ../WireMock.Net/WireMock.Net.snk
+ true
+ MIT
+ 10
+
+
+
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/WireMock.Net.Testcontainers/WireMockConfiguration.cs b/src/WireMock.Net.Testcontainers/WireMockConfiguration.cs
new file mode 100644
index 00000000..3403eca1
--- /dev/null
+++ b/src/WireMock.Net.Testcontainers/WireMockConfiguration.cs
@@ -0,0 +1,66 @@
+using Docker.DotNet.Models;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Configurations;
+using JetBrains.Annotations;
+
+namespace WireMock.Net.Testcontainers;
+
+///
+[PublicAPI]
+public sealed class WireMockConfiguration : ContainerConfiguration
+{
+#pragma warning disable CS1591
+ public string? Username { get; }
+
+ public string? Password { get; }
+
+ public bool HasBasicAuthentication => !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password);
+
+ public WireMockConfiguration(
+ string? username = null,
+ string? password = null
+ )
+ {
+ Username = username;
+ Password = password;
+ }
+#pragma warning restore CS1591
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public WireMockConfiguration(IResourceConfiguration resourceConfiguration) : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public WireMockConfiguration(IContainerConfiguration resourceConfiguration) : base(resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ public WireMockConfiguration(WireMockConfiguration resourceConfiguration) : this(new WireMockConfiguration(), resourceConfiguration)
+ {
+ // Passes the configuration upwards to the base implementations to create an updated immutable copy.
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The old Docker resource configuration.
+ /// The new Docker resource configuration.
+ public WireMockConfiguration(WireMockConfiguration oldValue, WireMockConfiguration newValue) : base(oldValue, newValue)
+ {
+ Username = BuildConfiguration.Combine(oldValue.Username, newValue.Username);
+ Password = BuildConfiguration.Combine(oldValue.Password, newValue.Password);
+ }
+}
\ No newline at end of file
diff --git a/src/WireMock.Net.Testcontainers/WireMockContainer.cs b/src/WireMock.Net.Testcontainers/WireMockContainer.cs
new file mode 100644
index 00000000..6532b07f
--- /dev/null
+++ b/src/WireMock.Net.Testcontainers/WireMockContainer.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using DotNet.Testcontainers.Containers;
+using JetBrains.Annotations;
+using Microsoft.Extensions.Logging;
+using RestEase;
+using Stef.Validation;
+using WireMock.Client;
+
+namespace WireMock.Net.Testcontainers;
+
+///
+/// A container for running WireMock in a docker environment.
+///
+public sealed class WireMockContainer : DockerContainer
+{
+ internal const int ContainerPort = 80;
+
+ private readonly WireMockConfiguration _configuration;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The container configuration.
+ /// The logger.
+ public WireMockContainer(WireMockConfiguration configuration, ILogger logger) : base(configuration, logger)
+ {
+ _configuration = Guard.NotNull(configuration);
+ }
+
+ ///
+ /// Gets the public Url.
+ ///
+ [PublicAPI]
+ public string GetPublicUrl() => GetPublicUri().ToString();
+
+ ///
+ /// Create a RestEase Admin client which can be used to call the admin REST endpoint.
+ ///
+ /// A
+ [PublicAPI]
+ public IWireMockAdminApi CreateWireMockAdminClient()
+ {
+ ValidateIfRunning();
+
+ var api = RestClient.For(GetPublicUri());
+
+ if (_configuration.HasBasicAuthentication)
+ {
+ api.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes($"{_configuration.Username}:{_configuration.Password}")));
+ }
+
+ return api;
+ }
+
+ ///
+ /// Create a which can be used to call this instance.
+ ///
+ /// An ordered list of System.Net.Http.DelegatingHandler instances to be invoked
+ /// as an System.Net.Http.HttpRequestMessage travels from the System.Net.Http.HttpClient
+ /// to the network and an System.Net.Http.HttpResponseMessage travels from the network
+ /// back to System.Net.Http.HttpClient. The handlers are invoked in a top-down fashion.
+ /// That is, the first entry is invoked first for an outbound request message but
+ /// last for an inbound response message.
+ ///
+ ///
+ [PublicAPI]
+ public HttpClient CreateClient(params DelegatingHandler[] handlers)
+ {
+ ValidateIfRunning();
+
+ var client = HttpClientFactory.Create(handlers);
+ client.BaseAddress = GetPublicUri();
+ return client;
+ }
+
+ ///
+ /// Create a (one for each URL) which can be used to call this instance.
+ /// The inner handler represents the destination of the HTTP message channel.
+ ///
+ /// An ordered list of System.Net.Http.DelegatingHandler instances to be invoked
+ /// as an System.Net.Http.HttpRequestMessage travels from the System.Net.Http.HttpClient
+ /// to the network and an System.Net.Http.HttpResponseMessage travels from the network
+ /// back to System.Net.Http.HttpClient. The handlers are invoked in a top-down fashion.
+ /// That is, the first entry is invoked first for an outbound request message but
+ /// last for an inbound response message.
+ ///
+ ///
+ [PublicAPI]
+ public HttpClient CreateClient(HttpMessageHandler innerHandler, params DelegatingHandler[] handlers)
+ {
+ ValidateIfRunning();
+
+ var client = HttpClientFactory.Create(innerHandler, handlers);
+ client.BaseAddress = GetPublicUri();
+ return client;
+ }
+
+ private void ValidateIfRunning()
+ {
+ if (State != TestcontainersStates.Running)
+ {
+ throw new InvalidOperationException("Unable to create HttpClient because the WireMock.Net is not yet running.");
+ }
+ }
+
+ private Uri GetPublicUri() => new UriBuilder(Uri.UriSchemeHttp, Hostname, GetMappedPublicPort(ContainerPort)).Uri;
+}
diff --git a/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs b/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs
new file mode 100644
index 00000000..d724b0e6
--- /dev/null
+++ b/src/WireMock.Net.Testcontainers/WireMockContainerBuilder.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Docker.DotNet.Models;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Configurations;
+using JetBrains.Annotations;
+using Stef.Validation;
+using WireMock.Net.Testcontainers.Models;
+
+namespace WireMock.Net.Testcontainers;
+
+///
+/// An specific fluent Docker container builder for WireMock.Net
+///
+public sealed class WireMockContainerBuilder : ContainerBuilder
+{
+ private readonly Dictionary _info = new()
+ {
+ { false, new ContainerInfo("sheyenrath/wiremock.net:latest", "/app/__admin/mappings") },
+ { true, new ContainerInfo("sheyenrath/wiremock.net-windows:latest", @"c:\app\__admin\mappings") }
+ };
+
+ private const string DefaultLogger = "WireMockConsoleLogger";
+
+ private readonly Lazy> _isWindowsAsLazy = new(async () =>
+ {
+ using var dockerClientConfig = TestcontainersSettings.OS.DockerEndpointAuthConfig.GetDockerClientConfiguration();
+ using var dockerClient = dockerClientConfig.CreateClient();
+
+ var version = await dockerClient.System.GetVersionAsync();
+ return version.Os.IndexOf("Windows", StringComparison.OrdinalIgnoreCase) > -1;
+ });
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public WireMockContainerBuilder() : this(new WireMockConfiguration())
+ {
+ DockerResourceConfiguration = Init().DockerResourceConfiguration;
+ }
+
+ ///
+ /// Automatically set the correct image (Linux or Windows) for WireMock which to create the container.
+ ///
+ /// A configured instance of
+ [PublicAPI]
+ public WireMockContainerBuilder WithImage()
+ {
+ var isWindows = _isWindowsAsLazy.Value.GetAwaiter().GetResult();
+ return WithImage(_info[isWindows].Image);
+ }
+
+ ///
+ /// Set the admin username and password for the container (basic authentication).
+ ///
+ /// The admin username.
+ /// The admin password.
+ /// A configured instance of
+ public WireMockContainerBuilder WithAdminUserNameAndPassword(string username, string password)
+ {
+ Guard.NotNull(username);
+ Guard.NotNull(password);
+
+ if (string.IsNullOrEmpty(username) && string.IsNullOrEmpty(password))
+ {
+ return this;
+ }
+
+ return Merge(DockerResourceConfiguration, new WireMockConfiguration(username, password))
+ .WithCommand($"--AdminUserName {username}", $"--AdminPassword {password}");
+ }
+
+ ///
+ /// Use the WireMockNullLogger.
+ ///
+ /// A configured instance of
+ [PublicAPI]
+ public WireMockContainerBuilder WithNullLogger()
+ {
+ return WithCommand("--WireMockLogger WireMockNullLogger");
+ }
+
+ ///
+ /// Defines if the static mappings should be read at startup (default set to false).
+ ///
+ /// A configured instance of
+ [PublicAPI]
+ public WireMockContainerBuilder WithReadStaticMappings()
+ {
+ return WithCommand("--ReadStaticMappings true");
+ }
+
+ ///
+ /// Watch the static mapping files + folder for changes when running.
+ ///
+ /// A configured instance of
+ [PublicAPI]
+ public WireMockContainerBuilder WithWatchStaticMappings(bool includeSubDirectories)
+ {
+ return WithCommand("--WatchStaticMappings true").WithCommand($"--WatchStaticMappingsInSubdirectories {includeSubDirectories}");
+ }
+
+ ///
+ /// Specifies the path for the (static) mapping json files.
+ ///
+ /// The path
+ ///
+ [PublicAPI]
+ public WireMockContainerBuilder WithMappings(string path)
+ {
+ Guard.NotNullOrEmpty(path);
+
+ var isWindows = _isWindowsAsLazy.Value.GetAwaiter().GetResult();
+
+ return WithReadStaticMappings().WithBindMount(path, _info[isWindows].MappingsPath);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Docker resource configuration.
+ private WireMockContainerBuilder(WireMockConfiguration dockerResourceConfiguration) : base(dockerResourceConfiguration)
+ {
+ DockerResourceConfiguration = dockerResourceConfiguration;
+ }
+
+ ///
+ protected override WireMockConfiguration DockerResourceConfiguration { get; }
+
+ ///
+ public override WireMockContainer Build()
+ {
+ Validate();
+
+ return new WireMockContainer(DockerResourceConfiguration, TestcontainersSettings.Logger);
+ }
+
+ ///
+ protected override WireMockContainerBuilder Init()
+ {
+ var builder = base.Init();
+
+ // In case no image has been set, set the image using internal logic.
+ if (builder.DockerResourceConfiguration.Image == null)
+ {
+ builder = builder.WithImage();
+ }
+
+ var isWindows = _isWindowsAsLazy.Value.GetAwaiter().GetResult();
+ var waitForContainerOS = isWindows ? Wait.ForWindowsContainer() : Wait.ForUnixContainer();
+ return builder
+ .WithPortBinding(WireMockContainer.ContainerPort, true)
+ .WithCommand($"--WireMockLogger {DefaultLogger}")
+ .WithWaitStrategy(waitForContainerOS.UntilMessageIsLogged("By Stef Heyenrath"));
+ }
+
+ ///
+ protected override WireMockContainerBuilder Clone(IContainerConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new WireMockConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override WireMockContainerBuilder Clone(IResourceConfiguration resourceConfiguration)
+ {
+ return Merge(DockerResourceConfiguration, new WireMockConfiguration(resourceConfiguration));
+ }
+
+ ///
+ protected override WireMockContainerBuilder Merge(WireMockConfiguration oldValue, WireMockConfiguration newValue)
+ {
+ return new WireMockContainerBuilder(new WireMockConfiguration(oldValue, newValue));
+ }
+}
\ No newline at end of file