C
C#16mo ago
Philémon

Test ASP.API controller with xUnit: can't assert Type

Hello 👋 I hope my question is not too dumb but I m stuck on the testing of my API controller. (It is my first test project) Me controller looks exactly like this :
[HttpGet]
public async Task<ActionResult<IEnumerable<RecipeDto>>> GetRecipes(
[FromQuery] int pageNumber,
[FromQuery] int pageSize,
[FromQuery] string? title,
[FromQuery] string? searchQuery)
{
try
{
(IEnumerable<Recipe> recipes, PaginationMetadata metadata) = await _recipeService.GetPage(pageNumber, pageSize, title, searchQuery);

if (recipes is null || recipes.Any() == false)
{
_logger.LogInformationGetAll(nameof(Recipe));
return NotFound();
}

IEnumerable<RecipeDto> response = recipes.Select(r => r.MapToRecipeDto());

Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(metadata));
return Ok(response);
}
catch (Exception ex)
{
_logger.LogCriticalGetAll(nameof(Recipe), ex);
return this.InternalErrorCustom();
}
}
[HttpGet]
public async Task<ActionResult<IEnumerable<RecipeDto>>> GetRecipes(
[FromQuery] int pageNumber,
[FromQuery] int pageSize,
[FromQuery] string? title,
[FromQuery] string? searchQuery)
{
try
{
(IEnumerable<Recipe> recipes, PaginationMetadata metadata) = await _recipeService.GetPage(pageNumber, pageSize, title, searchQuery);

if (recipes is null || recipes.Any() == false)
{
_logger.LogInformationGetAll(nameof(Recipe));
return NotFound();
}

IEnumerable<RecipeDto> response = recipes.Select(r => r.MapToRecipeDto());

Response.Headers.Add("X-Pagination", JsonSerializer.Serialize(metadata));
return Ok(response);
}
catch (Exception ex)
{
_logger.LogCriticalGetAll(nameof(Recipe), ex);
return this.InternalErrorCustom();
}
}
And i am trying to test the first case : Recipes.Any() = false, in this test :
[Fact]
public async Task GetRecipes_Empty_ReturnsNotFound()
{

// Arrange
Tools.RecipeService recipeService = new Tools.RecipeService();
RecipesController controller = new RecipesController(new NullLogger<RecipesController>(), recipeService);

// Act
ActionResult<IEnumerable<RecipeDto>> result = await controller.GetRecipes(1, 5, null, null);

// Assert
Assert.NotNull(result);
Assert.IsType<ActionResult<IEnumerable<RecipeDto>>>(result);
Assert.IsType<NotFoundResult>(result);
Assert.Equal(controller.NotFound(), result);
}
[Fact]
public async Task GetRecipes_Empty_ReturnsNotFound()
{

// Arrange
Tools.RecipeService recipeService = new Tools.RecipeService();
RecipesController controller = new RecipesController(new NullLogger<RecipesController>(), recipeService);

// Act
ActionResult<IEnumerable<RecipeDto>> result = await controller.GetRecipes(1, 5, null, null);

// Assert
Assert.NotNull(result);
Assert.IsType<ActionResult<IEnumerable<RecipeDto>>>(result);
Assert.IsType<NotFoundResult>(result);
Assert.Equal(controller.NotFound(), result);
}
But it doesnt work. Both my IsType<NotFoundResult>(result) and Equal(controller.NotFound(), result) fail. How could i test this please? 😁
51 Replies
Philémon
PhilémonOP16mo ago
Oh yes maybe i should say that i made a "test" recipe service in Tools.RecipeService, looking like this :
public async Task<(IEnumerable<Recipe> recipes, PaginationMetadata metadata)> GetPage(int pageNumber, int pageSize, string? title, string? searchQuery)
{
return await Task.FromResult((Enumerable.Empty<Recipe>(), new PaginationMetadata(1,5,0)));
}
public async Task<(IEnumerable<Recipe> recipes, PaginationMetadata metadata)> GetPage(int pageNumber, int pageSize, string? title, string? searchQuery)
{
return await Task.FromResult((Enumerable.Empty<Recipe>(), new PaginationMetadata(1,5,0)));
}
Philémon
PhilémonOP16mo ago
Test fails but they look the same to me 🥲
Philémon
PhilémonOP16mo ago
Ok sorry but I found the answer after being stuck for hours, it was as simple as :
ActionResult<IEnumerable<RecipeDto>> response = await controller.GetRecipes(1, 5, null, null);
**var result = response.Result;**
// Assert
Assert.NotNull(result);
Assert.IsType<NotFoundResult>(result);
ActionResult<IEnumerable<RecipeDto>> response = await controller.GetRecipes(1, 5, null, null);
**var result = response.Result;**
// Assert
Assert.NotNull(result);
Assert.IsType<NotFoundResult>(result);
I need to take the .Result of it 🤣
Angius
Angius16mo ago
Or better yet, await it
Philémon
PhilémonOP16mo ago
What do you think i should await ? I dont understand
Angius
Angius16mo ago
The thing you're trying to .Result
Mayor McCheese
Mayor McCheese16mo ago
I think I'm blind I don't see a .Result
Philémon
PhilémonOP16mo ago
"ActionResult" does not contain a definition for GetAwaiter it says var result = response.Result
Angius
Angius16mo ago
Angius
Angius16mo ago
Ah, response is not a Task?
Philémon
PhilémonOP16mo ago
i tried to put it in bold with two ** but it didnt work 🤣
Angius
Angius16mo ago
It's just a completely unrelated type that has a .Result property?
Philémon
PhilémonOP16mo ago
it is an action result
Angius
Angius16mo ago
If so, carry on
Philémon
PhilémonOP16mo ago
ok ok :p thank you !
Mayor McCheese
Mayor McCheese16mo ago
I'm totally blind; I don't see that
Philémon
PhilémonOP16mo ago
Here 😁
Mayor McCheese
Mayor McCheese16mo ago
oh okay NIT: I hate testing controllers this way
Philémon
PhilémonOP16mo ago
how would you do it ?
Mayor McCheese
Mayor McCheese16mo ago
WebApplicationFactory and call the endpoint
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;

public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}

[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.GetAsync(url);

// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
public class BasicTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;

public BasicTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}

[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
{
// Arrange
var client = _factory.CreateClient();

// Act
var response = await client.GetAsync(url);

// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Equal("text/html; charset=utf-8",
response.Content.Headers.ContentType.ToString());
}
}
https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-7.0 I personally will only unit test things that provide a quantifiable advantage The tests you're writing just make code harder to change and add negligible value
Philémon
PhilémonOP16mo ago
I had doubt that unit testing my api was really useful, for real I thought about integration test but it seems a step harder Maybe it will be more useful than unit testing I see
Mayor McCheese
Mayor McCheese16mo ago
so, by quantifiable, let's say you wrote conversions for dry ingredients from metric to imperial and vice versa that's worth testing
Philémon
PhilémonOP16mo ago
Yes i have none of that haha
Mayor McCheese
Mayor McCheese16mo ago
I get that; but if you did lets say that you had a recipe doubler as well so more ingredients might change time sorta thing and you have a really smart way to do that totally unit testable
Philémon
PhilémonOP16mo ago
Ok thank you a lot i see better now
Mayor McCheese
Mayor McCheese16mo ago
controller? meh; who cares; just call it like an entry point to your application same as a user would it can be harder
Philémon
PhilémonOP16mo ago
May i ask you, when you do integration tests, do you test on the real DB ? I was thinking of giving test data to my test project, using copy of my Seed class that seeds my DB with entity framework
Mayor McCheese
Mayor McCheese16mo ago
yesnt WebApplicationFactory has some tools to swap things out; it's worth some research so swap out the db for sqlite or something ( there are known compatability issues for some edge cases )
Philémon
PhilémonOP16mo ago
Ok thank you @Ruskell McCheese I will make integration tests, honnestly thats what i wanted to do at start 🤣 But i was kinda afraid the step was too big since i didnt make any test project yet
Mayor McCheese
Mayor McCheese16mo ago
but in a pipeline for instance; I might spin up a db in docker and seed with data and test against that
Philémon
PhilémonOP16mo ago
docker ❤️ i need to try this too that s a really good opportunity here
Mayor McCheese
Mayor McCheese16mo ago
some folks go for various "cobra" metrics; and it's a separate issue
Philémon
PhilémonOP16mo ago
i m HYPED for real
Mayor McCheese
Mayor McCheese16mo ago
cobra ~= perverse incentive so like, peeps will say, you need 90% code coverage; now you're in perverse incentive land meaning you'll do a lot of silly things to get there that aren't valuable just to get there ( not quite a perverse incentive, but )
Philémon
PhilémonOP16mo ago
oh so 90% is already too much if i understand well ? and you can use Cobra to get to the 90% ?
Mayor McCheese
Mayor McCheese16mo ago
The cobra bit, is that back in day in India, they started paying bounties on cobra snake heads, so now, perversely I'm going to just raise snakes and kill them for the bounty so at some point someone will say, "hey this is a bad idea we're just creating more snakes" and stop paying bounties so I'll let all my snakes go so now we have a larger snake problem
Philémon
PhilémonOP16mo ago
oh ok i see i will keep that in mind while doing my tests
Mayor McCheese
Mayor McCheese16mo ago
so using unit tests to get to high code coverage, now code is very ossified and hard to change
Philémon
PhilémonOP16mo ago
testing the most important, where the bugs are likely to be existing, right ?
Mayor McCheese
Mayor McCheese16mo ago
ossified == turning to stone
Philémon
PhilémonOP16mo ago
yes its like in french, os = bone ok nice
Mayor McCheese
Mayor McCheese16mo ago
This is the exciting part about something like web application factory; when you hit your controller, you're just going to also test everything in that stack
Philémon
PhilémonOP16mo ago
it looks like a lot of fun, i can learn docker and sql lite and the tests at the same time, 3 birds with one stone 😮
Mayor McCheese
Mayor McCheese16mo ago
with some creativity you can create tons of scenarios from a single test... Theory supports other types of "inputs" more than just inline data
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
[Theory]
[InlineData("/")]
[InlineData("/Index")]
[InlineData("/About")]
[InlineData("/Privacy")]
[InlineData("/Contact")]
and suddenly you have hundreds of tests for scenarios that actually make sense
Philémon
PhilémonOP16mo ago
i was thinking that my unit tests of api endpoint was kind of useless 🤣
Mayor McCheese
Mayor McCheese16mo ago
https://www.thomasbogholm.net/2021/12/01/xunit-using-theorydata-with-theory/ they can be valuable, but that will come with experience
Philémon
PhilémonOP16mo ago
Yes i guess, but not with endpoints like mine i think Thank you for all the resources and info
Mayor McCheese
Mayor McCheese16mo ago
hey no problem NB: this is a highly opinionated area results may vary
Philémon
PhilémonOP16mo ago
I see, but i know it will help me learn new things so it will be helpful to make tests that way, even if not every one agrees plus, its kind of what i wanted to do at start haha 😅
Becquerel
Becquerel16mo ago
Just dropping in to mention that if you are interested in using Docker to test database related stuff Testcontainers is a really cool package to check out makes creating and cleaning up Docker images real easy 🙂
Philémon
PhilémonOP16mo ago
I will check this out, thank you 😉
Want results from more Discord servers?
Add your server