How would you test a minimal api that uses a dictionary for saving entities?
-
Here's my Program.cs file (I've actually simplified it to some extent).
using StudentsMinimalApi;
using StudentsMinimalApi.Validation;
using System.Collections.Concurrent;WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
WebApplication app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
}app.UseStatusCodePages();
ConcurrentDictionary _students = new();
RouteGroupBuilder studentsApi = app.MapGroup("/student");
studentsApi.MapGet("/", () => _students);
RouteGroupBuilder studentsApiWithValidation = studentsApi
.MapGroup("/"); //I use a filter factory on this builder, but it does not matter for my question.studentsApiWithValidation.MapGet("/{id}", (string id) =>
_students.TryGetValue(id, out var student)
? TypedResults.Ok(student)
: Results.Problem(statusCode: 404));studentsApiWithValidation.MapPost("/{id}", (Student student, string id) =>
_students.TryAdd(id, student)
? TypedResults.Created($"/student/{id}", student)
: Results.ValidationProblem(new Dictionary
{
{ "id", new[] { "A student with the given id already exists." } }
}));app.Run();
public partial class Program { }
As you can see, I created an example minimal API that exposes endpoints that you can use to access or change data related to an example Student class using the HTTP protocol. So now I'd like to test my minimal API using xUnit. That's how I decided to test if the post method successfully creates a new student.
[Fact]
public async Task MapPost_Should_Successfully_Create_A_New_Student()
{
await using var application = new WebApplicationFactory();using HttpClient? client = application.CreateClient(); HttpResponseMessage? resultFromPost = await client.PostAsJsonAsync("/student/s1", new Student("X", "Y", "Z")); HttpResponseMessage? resultFromGet = await client.GetAsync("/student/s1"); Assert.Equal(HttpStatusCode.Created, resultFromPost.StatusCode); Assert.Equal(HttpStatusCode.OK, resultFromGet.StatusCode); string? contentAsString = await resultFromGet.Content.ReadAsStringAsync(); var contentAsStudentObject = JsonConvert.DeserializeObject(contentAsString); Assert.Equal(HttpStatusCode.Created, resultFromPost.StatusCode); Assert.Equal(HttpStatusCode.OK, resultFromGet.StatusCode); Assert.NotNull(content
-
Here's my Program.cs file (I've actually simplified it to some extent).
using StudentsMinimalApi;
using StudentsMinimalApi.Validation;
using System.Collections.Concurrent;WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
WebApplication app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
}app.UseStatusCodePages();
ConcurrentDictionary _students = new();
RouteGroupBuilder studentsApi = app.MapGroup("/student");
studentsApi.MapGet("/", () => _students);
RouteGroupBuilder studentsApiWithValidation = studentsApi
.MapGroup("/"); //I use a filter factory on this builder, but it does not matter for my question.studentsApiWithValidation.MapGet("/{id}", (string id) =>
_students.TryGetValue(id, out var student)
? TypedResults.Ok(student)
: Results.Problem(statusCode: 404));studentsApiWithValidation.MapPost("/{id}", (Student student, string id) =>
_students.TryAdd(id, student)
? TypedResults.Created($"/student/{id}", student)
: Results.ValidationProblem(new Dictionary
{
{ "id", new[] { "A student with the given id already exists." } }
}));app.Run();
public partial class Program { }
As you can see, I created an example minimal API that exposes endpoints that you can use to access or change data related to an example Student class using the HTTP protocol. So now I'd like to test my minimal API using xUnit. That's how I decided to test if the post method successfully creates a new student.
[Fact]
public async Task MapPost_Should_Successfully_Create_A_New_Student()
{
await using var application = new WebApplicationFactory();using HttpClient? client = application.CreateClient(); HttpResponseMessage? resultFromPost = await client.PostAsJsonAsync("/student/s1", new Student("X", "Y", "Z")); HttpResponseMessage? resultFromGet = await client.GetAsync("/student/s1"); Assert.Equal(HttpStatusCode.Created, resultFromPost.StatusCode); Assert.Equal(HttpStatusCode.OK, resultFromGet.StatusCode); string? contentAsString = await resultFromGet.Content.ReadAsStringAsync(); var contentAsStudentObject = JsonConvert.DeserializeObject(contentAsString); Assert.Equal(HttpStatusCode.Created, resultFromPost.StatusCode); Assert.Equal(HttpStatusCode.OK, resultFromGet.StatusCode); Assert.NotNull(content
You might want to research 'code coverage' tools. Those collect stats during your test to make sure that you have tested all code paths. Also note that you should not be actually testing the dictionary. But rather the usage of that. But perhaps that is what you mean.
Nikol Dimitrova 2023 wrote:
Is there a way for me to access the _students dictionary from the Program.cs file?
Presumably to test that the state is as excepted. Yes there are several. 1. Just publicly expose the dictionary (getter) 2. You can use reflection to get to the internals of a class. If you use the second then you should document with a comment that it only public for testing. For a small case like this I would use the first (getter). For larger libraries I would use reflection because the idiom is clearer with more usage and because more chance of public methods getting misused over time.
-
Here's my Program.cs file (I've actually simplified it to some extent).
using StudentsMinimalApi;
using StudentsMinimalApi.Validation;
using System.Collections.Concurrent;WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
WebApplication app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler();
}app.UseStatusCodePages();
ConcurrentDictionary _students = new();
RouteGroupBuilder studentsApi = app.MapGroup("/student");
studentsApi.MapGet("/", () => _students);
RouteGroupBuilder studentsApiWithValidation = studentsApi
.MapGroup("/"); //I use a filter factory on this builder, but it does not matter for my question.studentsApiWithValidation.MapGet("/{id}", (string id) =>
_students.TryGetValue(id, out var student)
? TypedResults.Ok(student)
: Results.Problem(statusCode: 404));studentsApiWithValidation.MapPost("/{id}", (Student student, string id) =>
_students.TryAdd(id, student)
? TypedResults.Created($"/student/{id}", student)
: Results.ValidationProblem(new Dictionary
{
{ "id", new[] { "A student with the given id already exists." } }
}));app.Run();
public partial class Program { }
As you can see, I created an example minimal API that exposes endpoints that you can use to access or change data related to an example Student class using the HTTP protocol. So now I'd like to test my minimal API using xUnit. That's how I decided to test if the post method successfully creates a new student.
[Fact]
public async Task MapPost_Should_Successfully_Create_A_New_Student()
{
await using var application = new WebApplicationFactory();using HttpClient? client = application.CreateClient(); HttpResponseMessage? resultFromPost = await client.PostAsJsonAsync("/student/s1", new Student("X", "Y", "Z")); HttpResponseMessage? resultFromGet = await client.GetAsync("/student/s1"); Assert.Equal(HttpStatusCode.Created, resultFromPost.StatusCode); Assert.Equal(HttpStatusCode.OK, resultFromGet.StatusCode); string? contentAsString = await resultFromGet.Content.ReadAsStringAsync(); var contentAsStudentObject = JsonConvert.DeserializeObject(contentAsString); Assert.Equal(HttpStatusCode.Created, resultFromPost.StatusCode); Assert.Equal(HttpStatusCode.OK, resultFromGet.StatusCode); Assert.NotNull(content
I had one QA type tell us we "had to" test every "if" and "else". And this was a "budgeting" system. There's a concept of "good enough" for the task (that QA can't deal with). I just have a "feeling" you've gone too far when you start asserting strings beyond "required" / not required. Checking the contents of fields is the job of "edit / validation" programs. (Which probably answers why we don't need to test "everything" as per QA). A "good design" (IMO) just requires fewer unit tests.
"Before entering on an understanding, I have meditated for a long time, and have foreseen what might happen. It is not genius which reveals to me suddenly, secretly, what I have to say or to do in a circumstance unexpected by other people; it is reflection, it is meditation." - Napoleon I
-
You might want to research 'code coverage' tools. Those collect stats during your test to make sure that you have tested all code paths. Also note that you should not be actually testing the dictionary. But rather the usage of that. But perhaps that is what you mean.
Nikol Dimitrova 2023 wrote:
Is there a way for me to access the _students dictionary from the Program.cs file?
Presumably to test that the state is as excepted. Yes there are several. 1. Just publicly expose the dictionary (getter) 2. You can use reflection to get to the internals of a class. If you use the second then you should document with a comment that it only public for testing. For a small case like this I would use the first (getter). For larger libraries I would use reflection because the idiom is clearer with more usage and because more chance of public methods getting misused over time.
Thank you for the response! May I ask you how exactly can we access the dictionary after let's say we add a student with a POST request using reflection? We can also check if the count (the number of elements of the dictionary) is correct (if we add one student, it should be 1; if we add 2 students, then it should be 2, and so on). I couldn't achieve that because we are actually using a WebApplicationBuilder. What is the approach? Also, if I use the first method, how exactly do I expose it with a getter? I guess I cannot use the top-level statements syntax then, because we cannot define properties in namespaces. Thanks!