Skip to content

Commit d725c59

Browse files
authored
Add Azure Functions HTTP trigger test helpers (#68)
* Add initial FastMoq.Azure test helpers * Fix Azure response XML docs * #44 Branch is to address Azure Testing Helpers * Add Azure storage client factory helpers * Add Azure Functions HTTP trigger test helpers * Document Azure Functions HTTP trigger helpers * Address PR review comments for Azure Functions helpers
1 parent c079901 commit d725c59

15 files changed

Lines changed: 1060 additions & 26 deletions

File tree

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ It wraps and extends mocking providers (currently Moq, with planned provider‑a
99
- Offer verification helpers and structured logging.
1010

1111
## 🛠 Tech Stack
12-
- **Language**: C# (.NET 6, 8, 9 targets)
12+
- **Language**: C# (.NET 8, 9, and 10 targets)
1313
- **Test frameworks**: xUnit (primary), with Moq as current default provider.
1414
- **Key namespaces**: `FastMoq.Core`, `FastMoq.Web`, `FastMoq.Web.Blazor`
1515
- **Patterns**: Constructor injection, provider abstraction, extension methods.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
using FastMoq.Extensions;
2+
using FastMoq.AzureFunctions.Http;
3+
using Microsoft.Azure.Functions.Worker;
4+
using Microsoft.Azure.Functions.Worker.Http;
5+
using System.Text;
6+
using System.Text.Json;
7+
8+
namespace FastMoq.AzureFunctions.Extensions
9+
{
10+
/// <summary>
11+
/// Provides Azure Functions HTTP-trigger helpers for constructing request and response data in tests.
12+
/// </summary>
13+
public static class HttpTriggerTestExtensions
14+
{
15+
/// <summary>
16+
/// Creates a concrete <see cref="HttpRequestData" /> for the current <see cref="Mocker" /> instance.
17+
/// </summary>
18+
/// <param name="mocker">The current <see cref="Mocker" /> instance.</param>
19+
/// <param name="configureRequest">Optional request-builder configuration.</param>
20+
/// <returns>A concrete <see cref="HttpRequestData" /> suitable for Azure Functions trigger tests.</returns>
21+
public static HttpRequestData CreateHttpRequestData(this Mocker mocker, Action<HttpRequestDataBuilder>? configureRequest = null)
22+
{
23+
ArgumentNullException.ThrowIfNull(mocker);
24+
25+
return GetOrCreateConfiguredFunctionContext(mocker).CreateHttpRequestData(configureRequest);
26+
}
27+
28+
/// <summary>
29+
/// Creates a concrete <see cref="HttpRequestData" /> for the supplied <see cref="FunctionContext" />.
30+
/// </summary>
31+
/// <param name="functionContext">The function context to associate with the request.</param>
32+
/// <param name="configureRequest">Optional request-builder configuration.</param>
33+
/// <returns>A concrete <see cref="HttpRequestData" /> suitable for Azure Functions trigger tests.</returns>
34+
public static HttpRequestData CreateHttpRequestData(this FunctionContext functionContext, Action<HttpRequestDataBuilder>? configureRequest = null)
35+
{
36+
ArgumentNullException.ThrowIfNull(functionContext);
37+
38+
var builder = new HttpRequestDataBuilder(functionContext);
39+
configureRequest?.Invoke(builder);
40+
return builder.Build();
41+
}
42+
43+
/// <summary>
44+
/// Creates a concrete <see cref="HttpResponseData" /> for the current <see cref="Mocker" /> instance.
45+
/// </summary>
46+
/// <param name="mocker">The current <see cref="Mocker" /> instance.</param>
47+
/// <param name="configureResponse">Optional response-builder configuration.</param>
48+
/// <returns>A concrete <see cref="HttpResponseData" /> suitable for Azure Functions trigger tests.</returns>
49+
public static HttpResponseData CreateHttpResponseData(this Mocker mocker, Action<HttpResponseDataBuilder>? configureResponse = null)
50+
{
51+
ArgumentNullException.ThrowIfNull(mocker);
52+
53+
return GetOrCreateConfiguredFunctionContext(mocker).CreateHttpResponseData(configureResponse);
54+
}
55+
56+
/// <summary>
57+
/// Creates a concrete <see cref="HttpResponseData" /> for the supplied <see cref="FunctionContext" />.
58+
/// </summary>
59+
/// <param name="functionContext">The function context to associate with the response.</param>
60+
/// <param name="configureResponse">Optional response-builder configuration.</param>
61+
/// <returns>A concrete <see cref="HttpResponseData" /> suitable for Azure Functions trigger tests.</returns>
62+
public static HttpResponseData CreateHttpResponseData(this FunctionContext functionContext, Action<HttpResponseDataBuilder>? configureResponse = null)
63+
{
64+
ArgumentNullException.ThrowIfNull(functionContext);
65+
66+
var builder = new HttpResponseDataBuilder(functionContext);
67+
configureResponse?.Invoke(builder);
68+
return builder.Build();
69+
}
70+
71+
/// <summary>
72+
/// Reads the current request body as a string and rewinds the stream when possible.
73+
/// </summary>
74+
/// <param name="request">The current request.</param>
75+
/// <param name="encoding">The text encoding. Defaults to UTF-8.</param>
76+
/// <returns>The body text.</returns>
77+
public static Task<string> ReadBodyAsStringAsync(this HttpRequestData request, Encoding? encoding = null)
78+
{
79+
ArgumentNullException.ThrowIfNull(request);
80+
81+
return ReadStreamAsStringAsync(request.Body, encoding);
82+
}
83+
84+
/// <summary>
85+
/// Reads the current request body as JSON and rewinds the stream when possible.
86+
/// </summary>
87+
/// <typeparam name="TValue">The expected JSON type.</typeparam>
88+
/// <param name="request">The current request.</param>
89+
/// <param name="jsonSerializerOptions">Optional serializer options.</param>
90+
/// <param name="cancellationToken">The cancellation token.</param>
91+
/// <returns>The deserialized body value.</returns>
92+
public static Task<TValue?> ReadBodyAsJsonAsync<TValue>(this HttpRequestData request, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
93+
{
94+
ArgumentNullException.ThrowIfNull(request);
95+
96+
return ReadStreamAsJsonAsync<TValue>(request.Body, jsonSerializerOptions, cancellationToken);
97+
}
98+
99+
/// <summary>
100+
/// Reads the current response body as a string and rewinds the stream when possible.
101+
/// </summary>
102+
/// <param name="response">The current response.</param>
103+
/// <param name="encoding">The text encoding. Defaults to UTF-8.</param>
104+
/// <returns>The body text.</returns>
105+
public static Task<string> ReadBodyAsStringAsync(this HttpResponseData response, Encoding? encoding = null)
106+
{
107+
ArgumentNullException.ThrowIfNull(response);
108+
109+
return ReadStreamAsStringAsync(response.Body, encoding);
110+
}
111+
112+
/// <summary>
113+
/// Reads the current response body as JSON and rewinds the stream when possible.
114+
/// </summary>
115+
/// <typeparam name="TValue">The expected JSON type.</typeparam>
116+
/// <param name="response">The current response.</param>
117+
/// <param name="jsonSerializerOptions">Optional serializer options.</param>
118+
/// <param name="cancellationToken">The cancellation token.</param>
119+
/// <returns>The deserialized body value.</returns>
120+
public static Task<TValue?> ReadBodyAsJsonAsync<TValue>(this HttpResponseData response, JsonSerializerOptions? jsonSerializerOptions = null, CancellationToken cancellationToken = default)
121+
{
122+
ArgumentNullException.ThrowIfNull(response);
123+
124+
return ReadStreamAsJsonAsync<TValue>(response.Body, jsonSerializerOptions, cancellationToken);
125+
}
126+
127+
private static FunctionContext GetOrCreateConfiguredFunctionContext(Mocker mocker)
128+
{
129+
var containsFunctionContext = mocker.Contains(typeof(FunctionContext));
130+
if (containsFunctionContext)
131+
{
132+
var existingFunctionContext = mocker.GetObject<FunctionContext>();
133+
if (existingFunctionContext?.InstanceServices is not null)
134+
{
135+
return existingFunctionContext;
136+
}
137+
}
138+
139+
if (mocker.HasTypeRegistration(typeof(IServiceProvider)))
140+
{
141+
mocker.AddFunctionContextInstanceServices(mocker.GetRequiredObject<IServiceProvider>(), replace: true);
142+
return mocker.GetRequiredObject<FunctionContext>();
143+
}
144+
145+
mocker.AddFunctionContextInstanceServices(replace: containsFunctionContext);
146+
return mocker.GetRequiredObject<FunctionContext>();
147+
}
148+
149+
private static async Task<TValue?> ReadStreamAsJsonAsync<TValue>(Stream stream, JsonSerializerOptions? jsonSerializerOptions, CancellationToken cancellationToken)
150+
{
151+
ResetStreamPosition(stream);
152+
var value = await JsonSerializer.DeserializeAsync<TValue>(stream, jsonSerializerOptions, cancellationToken).ConfigureAwait(false);
153+
ResetStreamPosition(stream);
154+
return value;
155+
}
156+
157+
private static async Task<string> ReadStreamAsStringAsync(Stream stream, Encoding? encoding)
158+
{
159+
ResetStreamPosition(stream);
160+
161+
using var reader = new StreamReader(stream, encoding ?? Encoding.UTF8, detectEncodingFromByteOrderMarks: true, leaveOpen: true);
162+
var value = await reader.ReadToEndAsync().ConfigureAwait(false);
163+
164+
ResetStreamPosition(stream);
165+
return value;
166+
}
167+
168+
private static void ResetStreamPosition(Stream stream)
169+
{
170+
if (stream.CanSeek)
171+
{
172+
stream.Position = 0;
173+
}
174+
}
175+
}
176+
}

FastMoq.AzureFunctions/FastMoq.AzureFunctions.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
<Company>$(Authors)</Company>
1111
<Copyright>Copyright(c) 2026 Christopher Winland</Copyright>
1212
<PackageId>$(AssemblyName)</PackageId>
13-
<Description>Azure Functions worker helpers for FastMoq, including typed FunctionContext.InstanceServices setup.</Description>
13+
<Description>Azure Functions worker and HTTP-trigger test helpers for FastMoq, including typed FunctionContext.InstanceServices setup, HttpRequestData/HttpResponseData builders, and body readers.</Description>
1414
<PackageProjectUrl>https://github.com/cwinland/FastMoq</PackageProjectUrl>
1515
<RepositoryUrl>https://github.com/cwinland/FastMoq</RepositoryUrl>
1616
<RepositoryType>git</RepositoryType>
17-
<PackageTags>FastMoq;AzureFunctions;FunctionContext;testing;mocking</PackageTags>
17+
<PackageTags>FastMoq;AzureFunctions;FunctionContext;HttpRequestData;HttpResponseData;testing;mocking</PackageTags>
1818
<PackageLicenseFile>license.txt</PackageLicenseFile>
1919
<PackageReadmeFile>README.md</PackageReadmeFile>
2020
<IsPackable>true</IsPackable>

0 commit comments

Comments
 (0)