C
C#•5mo ago
Dultus

How do I test a running API?

Hey, I'm using NUnit to test my application. Now, my application project consists of two parts: The application itself and an API server. I'm running a HttpClient.GetAsync command on the application using my localhost URL for the API. var response = client.GetAsync("http://localhost:5255/api/product/sort="+ (int)sort +"&filter="+ (int)filter).Result; I want to test this method or rather the API calls. How do I do that? AFAIK I can't test when the application is running which I have to to get my API running? I hope someone can help me. Thank you very much! 🙂
34 Replies
Angius
Angius•5mo ago
First and foremost throw that .Result off the bridge and use proper async/await Test the API as an API, not through the lens of the client
Anton
Anton•5mo ago
you probably want TestServer look it up
Angius
Angius•5mo ago
You want to mock API responses to test the client, and use integration testing with WebApplicationFactory to test the API As a side note, use string interpolation
Dultus
DultusOP•5mo ago
I thought to test it in case the URL changes for example. I have made 7 tests for the API itself. Will do as well, thank you! Hmm, I call a similar command within the constructor. How would I go about that?
Angius
Angius•5mo ago
Start by removing async code from the constructor Then await the async call
Dultus
DultusOP•5mo ago
Yeah but for that the method has to be async?
Angius
Angius•5mo ago
Yes
Dultus
DultusOP•5mo ago
public ProductViewModel()
{
await GetProductsAsync();
}
private async Task GetProductsAsync()
{
using (HttpClient client = new HttpClient())
{
var response = await client.GetAsync("http://localhost:5255/api/product");
response.EnsureSuccessStatusCode();
if (response != null)
{
var jsonString = response.Content.ReadAsStringAsync().Result;
Products = JsonConvert.DeserializeObject<List<ProductItem>>(jsonString);
}
}
return;
}
public ProductViewModel()
{
await GetProductsAsync();
}
private async Task GetProductsAsync()
{
using (HttpClient client = new HttpClient())
{
var response = await client.GetAsync("http://localhost:5255/api/product");
response.EnsureSuccessStatusCode();
if (response != null)
{
var jsonString = response.Content.ReadAsStringAsync().Result;
Products = JsonConvert.DeserializeObject<List<ProductItem>>(jsonString);
}
}
return;
}
How would I async the constructor?
Angius
Angius•5mo ago
You would not Long-running code should not be running in the constructor
Dultus
DultusOP•5mo ago
Ah, yeah, makes sense. I'll just create the object and call it afterwards.
Angius
Angius•5mo ago
(as a side note, you can safely depete the Newtonsoft dependency and just use the built-in JSON serializer) What is the client, btw? Some WPF app? A Blazor SPA?
Dultus
DultusOP•5mo ago
A Blazor Web Application
Angius
Angius•5mo ago
Ah, that makes things simple Use dependency injection Create a product service/repository/whatever, that will have the GetProductsAsync() method that returns a List<ProductItem> Inject the HttpClientFactory into that And inject that into whatever component needs those products By injecting the client factory you get the benefit of every call being able to use the same URL So should it change, you only change it in one place You could also consider using a typed client, which would elimnate the need for a service/repo layer
Dultus
DultusOP•5mo ago
public class ProductViewModel : INotifyPropertyChanged
{
private List<ProductItem> products;
public List<ProductItem> Products
{
get => products;
set
{
products = value;
OnPropertyChanged();
}
}

public ProductViewModel() { }
public async Task GetProductsAsync()
{
using (HttpClient client = new HttpClient())
{
var response = await client.GetAsync("http://localhost:5255/api/product");
response.EnsureSuccessStatusCode();
if (response != null)
{
var jsonString = response.Content.ReadAsStringAsync().Result;
Products = JsonConvert.DeserializeObject<List<ProductItem>>(jsonString);
}
}
return;
}
public class ProductViewModel : INotifyPropertyChanged
{
private List<ProductItem> products;
public List<ProductItem> Products
{
get => products;
set
{
products = value;
OnPropertyChanged();
}
}

public ProductViewModel() { }
public async Task GetProductsAsync()
{
using (HttpClient client = new HttpClient())
{
var response = await client.GetAsync("http://localhost:5255/api/product");
response.EnsureSuccessStatusCode();
if (response != null)
{
var jsonString = response.Content.ReadAsStringAsync().Result;
Products = JsonConvert.DeserializeObject<List<ProductItem>>(jsonString);
}
}
return;
}
And
@page "/products"
@using ProductDisplayerApp.ViewModels
@inject ProductViewModel ViewModel
@code {
protected override async Task OnInitializedAsync()
{
await ViewModel.GetProductsAsync();
}
public void Refresh()
{
StateHasChanged();
}
}
@page "/products"
@using ProductDisplayerApp.ViewModels
@inject ProductViewModel ViewModel
@code {
protected override async Task OnInitializedAsync()
{
await ViewModel.GetProductsAsync();
}
public void Refresh()
{
StateHasChanged();
}
}
Is how I have it right now.
Angius
Angius•5mo ago
Ah, huh, MVVM? Can't say I ever used it with Blazor
Dultus
DultusOP•5mo ago
I just try to have some sort of standard, not gonna lie. 😄 I'm not used to Blazor at all!
Angius
Angius•5mo ago
What's the Refresh() and StateHasChanged() do? Properties in Blazor are reactive by default
Dultus
DultusOP•5mo ago
They get called within the home file.
@if (isListView)
{
<ProductListView @ref="productListView" />
}
else{
<BottleView @ref="bottleView"/>
}
...
private void Filter()
{
// Load the filtered list of products into the current view model
if (filter == Models.Filter.Unfiltered)
{
filter = Models.Filter.Filtered;
}
else if (filter == Models.Filter.Filtered)
{
filter = Models.Filter.Unfiltered;
}
ViewModel.Sort_and_Filter(sortation, filter);
RefreshViews();
}
private void RefreshViews()
{
if (isListView)
{
productListView.Refresh();
}
else
{
bottleView.Refresh();
}
StateHasChanged();
}
@if (isListView)
{
<ProductListView @ref="productListView" />
}
else{
<BottleView @ref="bottleView"/>
}
...
private void Filter()
{
// Load the filtered list of products into the current view model
if (filter == Models.Filter.Unfiltered)
{
filter = Models.Filter.Filtered;
}
else if (filter == Models.Filter.Filtered)
{
filter = Models.Filter.Unfiltered;
}
ViewModel.Sort_and_Filter(sortation, filter);
RefreshViews();
}
private void RefreshViews()
{
if (isListView)
{
productListView.Refresh();
}
else
{
bottleView.Refresh();
}
StateHasChanged();
}
Because otherwise when I tried to filter or sort the view it did not update.
Angius
Angius•5mo ago
@inject ProductClient _client

<ul>
@foreach(var p in Products)
{
<li>@p.Name</li>
}
</ul>
<button @onclick="Load">Refresh</button>

@code {
private List<Product> Products { get; set; }

private async Task Load()
{
Products = await _client.GetAllProducts();
}

protected override async Task OnInitializedAsync()
{
await Load();
}
}
@inject ProductClient _client

<ul>
@foreach(var p in Products)
{
<li>@p.Name</li>
}
</ul>
<button @onclick="Load">Refresh</button>

@code {
private List<Product> Products { get; set; }

private async Task Load()
{
Products = await _client.GetAllProducts();
}

protected override async Task OnInitializedAsync()
{
await Load();
}
}
is probably how I'd do it If you need filtering or sorting, it would also be handled by that typed client/repo/service/whatever Make Load() public, give it arguments what to sort by and filter by Then call it from outside the component if need be From your sort dropdown component or w/e
Dultus
DultusOP•5mo ago
Sadly just saw that OnInitailized is getting called too late because the products need to be loaded into the grid before I load the page.
Angius
Angius•5mo ago
wtf why
Dultus
DultusOP•5mo ago
System.NullReferenceException: "Object reference not set to an instance of an object."
Because foreach'ing through the products throws me this error.
Angius
Angius•5mo ago
Ah, derp
private List<Product> Products { get; set; } = [];
private List<Product> Products { get; set; } = [];
There, solved It will start as an empty list, not a null
Dultus
DultusOP•5mo ago
Yeah, that works. @ZZZZZZZZZZZZZZZZZZZZZZZZZ One more question;
[HttpGet("product/sort={sort}&filter={filter}")]
public async Task<object> GetSortedProduct(int sort, int filter)
{
Sortation sortation = (Sortation)sort;
Filter filteration = (Filter)filter;
var ProductStorage = (await Product.GetProductAsync());


if (ProductStorage == null)
{
return StatusCode(404);
}


if (sortation != Sortation.Unsorted)
{
Product.Sort(ref ProductStorage, ref sortation);
}
if (filteration != Filter.Unfiltered)
{
Product.Filter(ref ProductStorage, ref filteration);
}
return Ok(ProductStorage);
}
[HttpGet("product/sort={sort}&filter={filter}")]
public async Task<object> GetSortedProduct(int sort, int filter)
{
Sortation sortation = (Sortation)sort;
Filter filteration = (Filter)filter;
var ProductStorage = (await Product.GetProductAsync());


if (ProductStorage == null)
{
return StatusCode(404);
}


if (sortation != Sortation.Unsorted)
{
Product.Sort(ref ProductStorage, ref sortation);
}
if (filteration != Filter.Unfiltered)
{
Product.Filter(ref ProductStorage, ref filteration);
}
return Ok(ProductStorage);
}
I'm perfectly fine not using async methods for sorting and filtering, right?
Angius
Angius•5mo ago
object 💀 And you have an await here, so this method does have to be async Also, I'd recommend doing the sorting and filtering on the database Not on the data that was fetched from the db Assuming Product.GetProductAsync() gets the products from the database, that is Ah, then that's fine
Dultus
DultusOP•5mo ago
It's just a JSON, so I'm a bit limited. 😄 This yes, just thinking about the Sort and Filter methods. Because I don't await those but could build them this way.
Angius
Angius•5mo ago
Do they do anything asynchronously?
Dultus
DultusOP•5mo ago
Not really. Just wondering if it affects the API itself.
Angius
Angius•5mo ago
If a method does not await anything it does not need to be async
Dultus
DultusOP•5mo ago
Alright, just thinking about performance. 😄 It's actually a project for a job interview so I want to be as precise as possible. Though I'm a bit unsure what to return if not an object - because I want status codes but also content to be send back. x) Should I build a wrapper around the objects to include the status code and the object?
Angius
Angius•5mo ago
ActionResult<List<Product>> for example Or even better, the typed result Result<OkResult<List<Product>>, NotFoundResult> As a side note, don't use StatusCode when you're returning a common code Return NotFound() instead Unauthorized() instead of 505, etc
Dultus
DultusOP•5mo ago
When it's async I'd need to wrap the result into a Task as well, right? Task<Result<OkResult<List<Product>>, NotFoundResult>>
Angius
Angius•5mo ago
So something like
[HttpGet("product/sort={sort}&filter={filter}")]
public async Results<Ok<List<Product>>, NotFound> GetSortedProduct(Sorting sort, Filter filter)
{
var products = await Product.GetProductAsync();

if (products is null)
{
return TypedResults.NotFound();
}

if (sort != Sortation.Unsorted)
{
Product.Sort(ref products, ref sort);
}
if (filter != Filter.Unfiltered)
{
Product.Filter(ref products, ref filter);
}
return TypedResults.Ok(products);
}
[HttpGet("product/sort={sort}&filter={filter}")]
public async Results<Ok<List<Product>>, NotFound> GetSortedProduct(Sorting sort, Filter filter)
{
var products = await Product.GetProductAsync();

if (products is null)
{
return TypedResults.NotFound();
}

if (sort != Sortation.Unsorted)
{
Product.Sort(ref products, ref sort);
}
if (filter != Filter.Unfiltered)
{
Product.Filter(ref products, ref filter);
}
return TypedResults.Ok(products);
}
Yep You can make it easier on yourself using a type alias
//...
using SortedProducts = Results<Ok<List<Product>>, NotFound>;

namespace MyApp;

[Route("[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
public async Task<SortedProducts> GetSortedProduct(Sorting sort, Filter filter)
{
//...
}
//...
}
//...
using SortedProducts = Results<Ok<List<Product>>, NotFound>;

namespace MyApp;

[Route("[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
public async Task<SortedProducts> GetSortedProduct(Sorting sort, Filter filter)
{
//...
}
//...
}
Actually sort and filter will automatically bind to query parameters anyway No need to make it a part of the path
Dultus
DultusOP•5mo ago
Ah, okay. Helps me a lot. Thank you!
Want results from more Discord servers?
Add your server