Skip to content

Instantly share code, notes, and snippets.

@indyone
Last active February 28, 2022 11:41
Show Gist options
  • Save indyone/3c376aad8ac1d8194aadbd5df3ca3e35 to your computer and use it in GitHub Desktop.
Save indyone/3c376aad8ac1d8194aadbd5df3ca3e35 to your computer and use it in GitHub Desktop.
HTTP server for mocking responses on unit tests

HTTP server for mocking responses on unit tests

This HTTP server can be used to mock the HTTP responses, requested by your client that you are testing.
The code is based on the System.Net.HttpListener class and this example.

There is also the accompanied extension methods so that you can easily and in a fluent way, configure the HTTP responses for each request.

Example

using var httpServer = new MockHttpServer(18080);
httpServer.Start();

httpServer
  .ForGetRequest("/items/1")
  .RespondWithOkJson(new { Id = 1, Hello = "World" });

var httpClient = new HttpClient();
var responseBody = await client.GetStringAsync($"{httpServer.BaseAddress}items/1");

// responseBody should be the JSON formatted string: {"Id":1,"Hello":"World"}
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace UnitTest.Utilities;
public class MockHttpServer : IDisposable
{
private readonly HttpListener _httpListener;
private volatile bool _keepGoing = true;
private Task _mainLoop;
private List<(Request Request, Response Response)> _responses = new List<(Request Request, Response Response)>();
public MockHttpServer(int port)
{
BaseAddress = $"http://localhost:{port}/";
_httpListener = new HttpListener
{
Prefixes =
{
BaseAddress
}
};
}
public string BaseAddress { get; private set; }
/// <summary>
/// Call this to start the web server
/// </summary>
public void Start()
{
if (_mainLoop != null && !_mainLoop.IsCompleted) return; // Already started
_mainLoop = MainLoop();
}
/// <summary>
/// Call this to stop the web server. It will not kill any requests currently being processed.
/// </summary>
public void Stop()
{
_keepGoing = false;
lock (_httpListener)
{
// Use a lock so we don't kill a request that's currently being processed
_httpListener.Stop();
}
try
{
_mainLoop.Wait();
}
catch
{
// Ignore
}
}
public void AddResponse(Request request, Response response)
{
_responses.Add((request, response));
}
/// <summary>
/// The main loop to handle requests into the HttpListener
/// </summary>
private async Task MainLoop()
{
_httpListener.Start();
while (_keepGoing)
{
try
{
// GetContextAsync() returns when a new request come in
var context = await _httpListener.GetContextAsync();
lock (_httpListener)
{
if (_keepGoing) ProcessRequest(context);
}
}
catch (Exception e)
{
// This gets thrown when the listener is stopped
if (e is HttpListenerException) return;
}
}
}
/// <summary>
/// Handle an incoming request
/// </summary>
private void ProcessRequest(HttpListenerContext context)
{
using (var response = context.Response)
{
try
{
var handled = false;
foreach (var tuple in _responses)
{
if (tuple.Request.Method != context.Request.HttpMethod)
{
continue;
}
if (tuple.Request.Path != context.Request.RawUrl)
{
continue;
}
response.StatusCode = (int)tuple.Response.StatusCode;
response.ContentType = tuple.Response.ContentType;
if (tuple.Response.Body != null)
{
response.ContentLength64 = tuple.Response.Body.Length;
response.OutputStream.Write(Encoding.UTF8.GetBytes(tuple.Response.Body));
}
handled = true;
break;
}
if (!handled)
{
response.StatusCode = 404;
}
}
catch (Exception ex)
{
response.StatusCode = 500;
response.ContentType = "application/json";
var buffer = Encoding.UTF8.GetBytes(ex.ToString());
response.ContentLength64 = buffer.Length;
response.OutputStream.Write(buffer, 0, buffer.Length);
}
}
}
public record Request(string Method, string Path);
public record Response(HttpStatusCode StatusCode, string Body, string ContentType);
public void Dispose()
{
Stop();
}
}
using System.Net;
using System.Net.Http;
using System.Text.Json;
namespace UnitTest.Utilities;
public static class MockHttpServerExtensions
{
public static (MockHttpServer, MockHttpServer.Request) ForRequest(this MockHttpServer httpServer, HttpMethod method, string path)
{
var request = new MockHttpServer.Request(method.Method, path);
return (httpServer, request);
}
public static (MockHttpServer, MockHttpServer.Request) ForGetRequest(this MockHttpServer httpServer, string path)
{
return httpServer.ForRequest(HttpMethod.Get, path);
}
public static (MockHttpServer, MockHttpServer.Request) ForPostRequest(this MockHttpServer httpServer, string path)
{
return httpServer.ForRequest(HttpMethod.Post, path);
}
public static MockHttpServer RespondWith(this (MockHttpServer, MockHttpServer.Request) mockSetup, HttpStatusCode statusCode, string body, string contentType)
{
var response = new MockHttpServer.Response(statusCode, body, contentType);
mockSetup.Item1.AddResponse(mockSetup.Item2, response);
return mockSetup.Item1;
}
public static MockHttpServer RespondWithNotFound(this (MockHttpServer, MockHttpServer.Request) mockSetup)
{
return mockSetup.RespondWith(HttpStatusCode.NotFound, null, "application/json");
}
public static MockHttpServer RespondWithOkJson(this (MockHttpServer, MockHttpServer.Request) mockSetup, object body)
{
return mockSetup.RespondWith(HttpStatusCode.OK, JsonSerializer.Serialize(body), "application/json");
}
public static MockHttpServer RespondWithServerErrorJson(this (MockHttpServer, MockHttpServer.Request) mockSetup, object body)
{
return mockSetup.RespondWith(HttpStatusCode.InternalServerError, JsonSerializer.Serialize(body), "application/json");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment