C
C#2y 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émonOP2y 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émonOP2y ago
Test fails but they look the same to me 🥲
Philémon
PhilémonOP2y 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
Angius2y ago
Or better yet, await it
Philémon
PhilémonOP2y ago
What do you think i should await ? I dont understand
Angius
Angius2y ago
The thing you're trying to .Result
Mayor McCheese
I think I'm blind I don't see a .Result
Philémon
PhilémonOP2y ago
"ActionResult" does not contain a definition for GetAwaiter it says var result = response.Result
Angius
Angius2y ago
Angius
Angius2y ago
Ah, response is not a Task?
Philémon
PhilémonOP2y ago
i tried to put it in bold with two ** but it didnt work 🤣
Angius
Angius2y ago
It's just a completely unrelated type that has a .Result property?
Philémon
PhilémonOP2y ago
it is an action result
Angius
Angius2y ago
If so, carry on
Philémon
PhilémonOP2y ago
ok ok :p thank you !
Mayor McCheese
I'm totally blind; I don't see that
Philémon
PhilémonOP2y ago
Here 😁
Mayor McCheese
oh okay NIT: I hate testing controllers this way
Philémon
PhilémonOP2y ago
how would you do it ?
Mayor McCheese
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émonOP2y 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
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émonOP2y ago
Yes i have none of that haha
Mayor McCheese
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émonOP2y ago
Ok thank you a lot i see better now
Mayor McCheese
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émonOP2y 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
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émonOP2y 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
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émonOP2y ago
docker ❤️ i need to try this too that s a really good opportunity here
Mayor McCheese
some folks go for various "cobra" metrics; and it's a separate issue
Philémon
PhilémonOP2y ago
i m HYPED for real
Mayor McCheese
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émonOP2y ago
oh so 90% is already too much if i understand well ? and you can use Cobra to get to the 90% ?
Mayor McCheese
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émonOP2y ago
oh ok i see i will keep that in mind while doing my tests
Mayor McCheese
so using unit tests to get to high code coverage, now code is very ossified and hard to change
Philémon
PhilémonOP2y ago
testing the most important, where the bugs are likely to be existing, right ?
Mayor McCheese
ossified == turning to stone
Philémon
PhilémonOP2y ago
yes its like in french, os = bone ok nice
Mayor McCheese
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émonOP2y 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
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émonOP2y ago
i was thinking that my unit tests of api endpoint was kind of useless 🤣
Mayor McCheese
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émonOP2y ago
Yes i guess, but not with endpoints like mine i think Thank you for all the resources and info
Mayor McCheese
hey no problem NB: this is a highly opinionated area results may vary
Philémon
PhilémonOP2y 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
becquerel2y 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émonOP2y ago
I will check this out, thank you 😉

Did you find this page helpful?