Releases: MetalHexx/RadEndpoints
RadEndpoints Unit Testing
RadEndpoints now provides a comprehensive testing infrastructure that makes unit testing endpoints feel familiar to developers who have worked with ASP.NET Core Minimal APIs.
The testing framework uses the same TypedResults pattern that you're already familiar with, making tests easy to write and understand.
Overview
The RadEndpoints testing framework consists of two main components:
EndpointFactory- Creates testable endpoint instances with mocked dependencies.TypedResultsTestExtensions- Provides endpoint extension methods to extract and verify TypedResults.
Philosophy
The RadEndpoints testing framework is designed with these principles in mind:
- Familiar Patterns - Uses the same TypedResults pattern developers know from Minimal APIs
- Type Safety - Strongly-typed result extraction with proper casting
- Comprehensive Coverage - Supports most HTTP result types and scenarios
- Easy Mocking - Simple dependency injection for testing different scenarios
- Clear Assertions - Intuitive extension methods for common testing patterns
Quick Start Example
Here's a simple example showing how to unit test a RadEndpoint:
Example Endpoint Implementation
public class GetItemRequest
{
[FromRoute]
public int Id { get; set; }
}
public class GetItemResponse
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Message { get; set; } = "Item retrieved successfully";
}
public class GetItemEndpoint : RadEndpoint<GetItemRequest, GetItemResponse>
{
public override void Configure()
{
Get("/items/{id}")
.Produces<GetItemResponse>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound)
.WithDocument(tag: "Items", desc: "Get an item by ID");
}
public override async Task Handle(GetItemRequest r, CancellationToken ct)
{
// ... async data retrieval
if (r.Id <= 0)
{
SendNotFound("Item not found");
return;
}
Response = new GetItemResponse
{
Id = r.Id,
Name = $"Item {r.Id}"
};
Send();
}
}Unit Test Example
[Fact]
public async Task When_ValidId_ShouldReturnItem()
{
// Arrange
var endpoint = EndpointFactory.CreateEndpoint<GetItemEndpoint>();
var request = new GetItemRequest { Id = 1 };
// Act
await endpoint.Handle(request, CancellationToken.None);
// Assert
var result = endpoint.GetResult<Ok<GetItemResponse>>();
result.Value.Id.Should().Be(1);
result.Value.Name.Should().Be("Item 1");
endpoint.GetStatusCode().Should().Be(HttpStatusCode.OK);
}
[Fact]
public async Task When_InvalidId_ShouldReturnNotFound()
{
// Arrange
var endpoint = EndpointFactory.CreateEndpoint<GetItemEndpoint>();
var request = new GetItemRequest { Id = 0 };
// Act
await endpoint.Handle(request, CancellationToken.None);
// Assert
var result = endpoint.GetResult<NotFound<string>>();
result.Value.Should().Be("Item not found");
endpoint.GetStatusCode().Should().Be(HttpStatusCode.NotFound);
}Using EndpointFactory
The EndpointFactory allows you to create testable instances of your endpoints with default or custom mocked dependencies.
Creating an Endpoint with Default Mocks
By default, EndpointFactory provides default auto-mocked dependencies for the following:
- ILogger
- IHttpContextAccessor (HttpContext)
- IWebHostEnvironment
var endpoint = EndpointFactory.CreateEndpoint<MyEndpoint>();This is the simplest way to create an endpoint for testing. The default mocks are sufficient for many unit tests where you're not directly testing these dependencies.
Creating an Endpoint with Custom Mocks
If you need to test specific scenarios that involve these dependencies, you can provide your own custom mocks. For example, you might want to verify logging behavior or simulate different HTTP contexts.
You can provide custom dependencies to test specific scenarios. For example:
var customLogger = Substitute.For<ILogger<MyEndpoint>>();
var customContext = Substitute.For<IHttpContextAccessor>();
var customEnvironment = Substitute.For<IWebHostEnvironment>();
var endpoint = EndpointFactory.CreateEndpoint(
logger: customLogger,
httpContextAccessor: customContext,
webHostEnvironment: customEnvironment
);Passing Constructor Arguments
You can also simply pass mocks for the constructor dependencies. Doing so will automatically provide the default mocks for ILogger, IHttpContextAccessor, and IWebHostEnvironment.
var endpoint = EndpointFactory.CreateEndpoint<MyEndpoint>
(
Substitute.For<IMyCustomService>(),
Substitute.For<IOtherDependency>(),
...any other constructor dependencies
);Creating an Endpoint with All Dependencies
You can create an endpoint with all possible dependencies, including the ILogger, IHttpContextAccessor, IWebHostEnvironment and any other constructor dependencies your endpoint relies on. For example:
var endpoint = EndpointFactory.CreateEndpoint<MyEndpoint>
(
Substitute.For<ILogger<MyEndpoint>>(),
Substitute.For<IHttpContextAccessor>(),
Substitute.For<IWebHostEnvironment>(),
Substitute.For<IMyCustomService>(),
Substitute.For<IOtherDependency>()
);Example: Testing with Custom Logger
[Fact]
public async Task When_CustomLoggerProvided_ShouldLogCorrectly()
{
// Arrange
var customLogger = Substitute.For<ILogger<MyEndpoint>>();
var endpoint = EndpointFactory.CreateEndpoint<MyEndpoint>(logger: customLogger);
// Act
await endpoint.Handle(new MyRequest(), CancellationToken.None);
// Assert
customLogger.Received(1).Log(
LogLevel.Information,
Arg.Any<EventId>(),
Arg.Is<object>(o => o.ToString()!.Contains("Expected log message")),
Arg.Any<Exception>(),
Arg.Any<Func<object, Exception?, string>>()
);
}Example: Testing with Custom HTTP Context
[Fact]
public async Task When_CustomHttpContextProvided_ShouldUseIt()
{
// Arrange
var customContext = Substitute.For<IHttpContextAccessor>();
var httpContext = new DefaultHttpContext();
httpContext.Request.Headers.Append("mock-header", "mock-value");
httpContext.Request.Method = "POST";
customContext.HttpContext.Returns(httpContext);
var endpoint = EndpointFactory.CreateEndpoint<MyEndpoint>(httpContextAccessor: customContext);
// Act
await endpoint.Handle(new MyRequest(), CancellationToken.None);
// Assert
Assert.Equal("POST", endpoint.HttpContext.Request.Method);
// Assert something that depends on the custom header
}These examples demonstrate how to use EndpointFactory to create endpoints with default or custom dependencies, making it easy to test various scenarios in isolation.
RadEndpoints TypedResults Testing
The TypedResultsTestExtensions class provides a set of extension methods designed to simplify unit testing on responses for RadEndpoint instances.
These methods enable developers to easily extract and verify result payloads and HTTP status codes using the familiar Minimal API TypedResults pattern.
Key features include:
- Result Extraction: Retrieve specific result types (e.g.,
Ok<T>,Created<T>,NotFound<T>) - Problem Handling: Access problem results (e.g.,
ValidationProblem,ProblemHttpResult) for error scenarios. - Status Code Verification: Check the HTTP status code associated with the endpoint's response.
Available Extension Methods
The TypedResultsTestExtensions class provides several extension methods for testing:
Result Extraction
endpoint.GetResult<T>()- Gets specific TypedResult types (Ok, Created, NotFound, etc.)endpoint.GetProblem<T>()- Gets problem results (ProblemHttpResult, ValidationProblem, IRadProblem implementations)endpoint.HasResult()- Returns true if endpoint set any resultendpoint.HasProblem()- Returns true if endpoint set any problem
RadTestException Throwing
- If the expected result type is not found with
GetResult<T>orGetProblem<T>, these methods throw aRadTestExceptionwith a descriptive message to help diagnose test failures.
HTTP Status Code Checking
endpoint.GetStatusCode()- Gets the HTTP status code from any result type
Supported TypedResults
The testing framework supports all ASP.NET Core TypedResults:
Success Results
Ok<T>- 200 OK responsesCreated<T>- 201 Created responsesAccepted<T>- 202 Accepted responses
Error Results
NotFound<T>- 404 Not Found responsesConflict<T>- 409 Conflict responsesProblemHttpResult- Problem details responsesValidationProblem- 400 Validation error responses
Authentication Results
UnauthorizedHttpResult- 401 authentication challengesForbidHttpResult- 403 authorization failures
File Results
FileContentHttpResult- Byte array file responsesFileStreamHttpResult- Stream file responsesPhysicalFileHttpResult- Physical file responses
Navigation Results
RedirectHttpResult- Redirect responses
Result Testing Examples
Many different scenarios are possible, but here are a few examples:
Success Response
[Fact]
public async Task When_EndpointCalled_ReturnsSuccess()
{
// Arrange
var request = new TestRequest { TestProperty = 5 };
var endpoint = EndpointFactory.CreateEndpoint<TestOkEndpoint>();
// Act
await endpoint.Handle(re...RadEndpoints.Testing: JsonSerializer Options
- Added the ability to pass custom json serializer options to the RadEndpoints.Testing.RadTestClientExtensions class.
Endpoint Implementation
- CustomJsonEndpoint.cs - Shows the endpoint setup with request/response models and enum handling
Test Examples
- CustomJsonSerializationTests.cs - Complete test suite demonstrating:
- Default serialization behavior
- Custom headers with JSON requests
- Validation with custom JSON options
- CamelCase property naming
- Enum string conversion
Server Configuration
- Program.cs - Server-side JSON configuration that matches the test client options
Properly handle empty strings in test request parameters
Fixed RadRequestBuilder to include empty string values in query parameters,
headers, and form fields instead of excluding them and sending null values. This ensures test requests accurately reflect real HTTP behavior and enables proper testing
of empty string validation scenarios.
- Query parameters: empty strings now included in query string
- Headers: empty string values now added to request headers
- Form fields: empty strings now included in form data
- Added comprehensive test coverage for empty string parameter handling
Improved Endpoint Unit Testing
Improved Endpoint Unit Testing
- Added the ability to mock ILogger, IHttpContextAccessor, and IWebHostEnvironment using the EndpointFactory.
- You can see an example of how to use it by looking at the FactoryTestEndpoint and UnitTestFactoryTests classes.
1.0.0-alpha.17
Test Helper Nullable Request Property Fix
- Request builder in RadEndpoints.Testing nuget package was causing nullable request types to not be properly built by producing null values (even when they weren't) into generated HttpRequests in integration tests.
- Symptom was only present when using the package in the context of another project outside of this repo.
- Technical Details:
- The reflection-based request builder was failing to process nullable properties
(e.g., string?) correctly when used in other applications. This was due to missing
nullable context metadata, which caused binding attribute detection (e.g., [FromBody])
to silently fail. The fix ensures attributes are evaluated before skipping null values,
and type checks useispattern matching to avoid runtime mismatches.
- The reflection-based request builder was failing to process nullable properties
1.0.0-alpha.16
Features
- RadEndpoints (both with and without request) now support returning full ProblemDetails and ValidationProblem models.
- Example: ProblemExampleEndpoint
- Example: ValidationProblemExampleEndpoint
1.0.0-alpha.15
Fixes
- RadResponseAssertions "WithKeyAndValue()"
- Now properly asserts the key and value on the ValidationProblemDetails return type.
- Added test to cover it.
1.0.0-alpha.14
Fixes
- Random integration test failures from collisions when tests running in parallel.
- Static Endpoint registration collection was causing collisions when multiple tests spun up a new app process.
- Switched to thread-safe collection.
- Automatic Validator Registration was updated for Net 8 compatibility.
- This is necessary due to changes in AOT compilation which doesn't work with my previous approach.
Updates
- Endpoint Registry is now registered as singleton
- Will be more efficient than scoped.
- Safe since endpoint registrations don't changed after the service bootstrapping completes.
1.0.0-alpha.13
Feature: Endpoint Unit Testing
- Added a helper factory to generate easy to unit test endpoints
- See: Example Unit Tests
1.0.0-alpha.11
Feature: POST Without Request Integration Test Helper
- Added a new test extension that supports deserialization of response models when the endpoint has no request model.
- See: Test Example
- Thanks for reporting @letsandeepio!