Need to test GET that have Json as a response. I haven't found useful info in official documentation.
Feature: API_Retrieving_Platforms
As an authorized user...
#mytag
Scenario: Perform Get request
Given I am an authorized user
When I perform GET request "/api/hotels/lists/platforms",
Then I receive a JSON in response:
"""
[
{
"refId": 1,
"label": "Mobile"
},
{
"refId": 2,
"label": "Desktop"
}
]
"""
The step for retrieving Json is:
[Then(#"I receive a JSON in response:")]
public void ThenIReceiveAJSONInResponse(string JSON)
{
Assert.Equal(HttpStatusCode.OK, _responseMessage.StatusCode);
}
How to parse this?
In short: You're Cuking It Wrong.
The longer answer is that you need to write your steps differently so you remove the technical jargon from it, and focus on the business value.
Scenario: Retriving a list of platforms
Given I am an authorized user
When I retrieve a list of hotel platforms
Then I should receive the following hotel platforms:
| Platform |
| Mobile |
| Desktop |
Step: When I retrieve a list of hotel platforms
This step should make the GET request in C# code. Save the response of that GET request in the Scenario context.
Step: Then I should receive the following hotel platforms:
Makes a simple assertion, and omits technical information like the "Ref Id". The platform name is all you really care about.
A rough start to those steps would be:
using TechTalk.SpecFlow;
using TechTalk.SpecFlow.Assist;
[Binding]
public class PlatformSteps
{
private readonly ScenarioContext scenario;
/// <summary>
/// Gets or sets the hotel platforms on the scenario context
/// </summary>
private IEnumerable<SomeJsonResponseType> HotelPlatforms
{
get => (IEnumerable<SomeJsonResponseType>)scenario["HotelPlatformsResponse"];
set => scenario["HotelPlatformsResponse"] = value;
}
public PlatformSteps(ScenarioContext scenario)
{
this.scenario = scenario;
}
[When(#"^When I retrieve a list of hotel platforms")]
public void WhenIRetrieveAListOfHotelPlatforms()
{
HotelPlatforms = api.GetHotelPlatforms(); // Or whatever you API call looks like
}
[Then(#"^I should receive the following hotel platforms:")]
public void IShouldReceiveTheFollowingHotelPlatforms(Table table)
{
var actualPlatforms = HotelPlatforms.Select(r => r.PlatformName);
table.CompareToSet(actualPlatforms);
}
}
One way to improve this is by not putting the exact JSON in the Specflow step.
I'd suggest using something like
Then I receive a response that looks like 'myResponsefile.json'
You could then create step which processes the response and looks at a file in your repo to compare it with
[Then(#"I receive a response that looks like '(.*)'")]
public void IreceiveAJsonResponse(string responsefilenametocompare)
{
string receivedjson = GetMyReceivedjsonObject();
string filePathAndName = "myfile.json";
string json = File.ReadAllText(filePathAndName);
JToken expected = JToken.Parse(json);
JToken actual = JToken.Parse(receivedjson);
actual.Should().BeEquivalentTo(expected);
}
Related
I have a Asp.Net 6+ Web Api that has two endpoints doing almost exactly the same thing :
- the first one gets its parameters automagically from Asp.Net . I didn't give it a second thought: it accepts parameters from the POST's body and it's Asp.Net that does the deserialization, via System.Text.Json internally.
[HttpPost]
[Route("public/v1/myRoute/")]
public async Task<ActionResult> Import(IEnumerable<JsonItemModel> items) {
// the items are already ready to use.
FooProcessItems(items);
}
- the second one receives an IFormFile in a form data (the end-user uploads a file by using a button in the UI), gets the stream, and deserializes it "manually" using System.Text.JsonSerializer.DeserializeAsync.
[HttpPost]
[Route("public/v1/myRouteWithFile/")]
public async Task<ActionResult<Guid>> ImportWithFile([FromForm] MyFormData formData
) {
var stream = formaData.File.OpenReadStream();
var items = await JsonSerializer.DeserializeAsync<IEnumerable<JsonItemModel>>(file);
FooProcessItems(items);
}
My question :
I want to customize the deserialization process (to add some constraints such as "this field cannot be null", etc.) and I want both methods to produce exactly the same result.
How do I do that?
Is it simply a case of adding Json decorators in the model and letting .Net do the rest?
public class JsonItemModel {
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] // <-- Some custom constraint that will be picked up both by Deserialize and the POST endpoint.
public int SomeField { get; init; } = 0;
...
}
I would like to filter a list of vehicles, by their makeId using httpGet. The URL I would expect to use is:
https://localhost:5001/api/vehicle?makeId=2
Below, I will define the DTO and controller methods I used for this task:
FilterDto
public class FilterDTO
{
public int? MakeId { get; set; }
}
Below are the 2 HTTPGet methods in my controller class. I expect the first method to be called.
[HttpGet]
public async Task<IEnumerable<VehicleDTO>> Get(FilterDTO filterDto)
{
var filter = _mapper.Map<Filter>(filterDto);
var vehicles = await _vehicleRepository.GetAll(filter);
return _mapper.Map<IEnumerable<VehicleDTO>>(vehicles);
}
[HttpGet("{id}")]
public async Task<ActionResult<VehicleDTO>> Get(long id)
{
var vehicle = await _vehicleRepository.GetWithRelated(id);
if (vehicle == default)
{
return BadRequest("Vehicle not found");
}
var result = _mapper.Map<VehicleDTO>(vehicle);
return Ok(result);
}
With the above code, when I call the URL above, in Postman I get a 400 Error, saying "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."
I get the same result for https://localhost:5001/api/vehicle
If I change the first Get method like below, I am able to get a response:
[HttpGet]
public async Task<IEnumerable<VehicleDTO>> Get(int? makeId)
{
var filter = new Filter { MakeId = makeId};
var vehicles = await _vehicleRepository.GetAll(filter);
return _mapper.Map<IEnumerable<VehicleDTO>>(vehicles);
}
After this (lengthy) introduction, my questions are:
Why does HttpGet support 'int?' but not the data transfer object 'FilterDto'?
Should I be using a different verb instead of HttpGet?
I might have to filter in the future for some other types (say customerId). Is there any way I can change the method to support custom objects, like FilterDto, ideally without changing the verb?
Change your code as follow:
[HttpGet]
public async Task<IEnumerable<VehicleDTO>> Get([FromQuery] FilterDTO filterDto)
{
var filter = _mapper.Map<Filter>(filterDto);
var vehicles = await _vehicleRepository.GetAll(filter);
return _mapper.Map<IEnumerable<VehicleDTO>>(vehicles);
}
and call it like:
baseUrl/Controller/Get?MarkId=1
Take a look at the docs.
Basically the primitive types are supported, but the controller has no idea how to convert your web request data into C# object. You need to explicitly tell it how you want this custom object to be created out of web request.
You may have in mind that HttpGet methods are only able to receive primitiveTypes (string, int, short, datetime -using a specific format-) because the arguments are being sent through query string, for example:
myAddres.com/api/mymethod?id=5&filter1=value1&filter2=value2
Having this consideration in mind you'll notice there's no way to send any object because you need to use a json or another notation, remember querystring has a limit and because of that is better using "argument=value" notation.
On the other hand PUT and POST are able to send their data through a "body" property where you may use a json notation and this way you may create almost any object on your Backend side.
If you need to use an object as an argument it is a better idea using POST or PUT (better POST than PUT).
I just leverage default Application Insights logging to log the request telemetry without ANY custom code.
The request telemetry looks like this:
timestamp [UTC] 2019-12-19T00:22:10.2563938Z
id |a758472d124b6e4688a33b2ad9755f33.b3979544_
name GET MyMethod [type]
url https://xxxx
success True
resultCode 200
duration 153.2676
performanceBucket <250ms
itemType request
customDimensions
AppId xxxx-xxxx-xxxx-xxxx-xxxxxx
AspNetCoreEnvironment: west us
_MS.ProcessedByMetricExtractors (Name:'Requests', Ver:'1.1')
Now I want to add a new property to customDimensions in Request telemetry, say, correlationId. What is the easiest way to to it? I just want to expend the existing request telemetry, don't want to create new event.
If you're interested in massaging data (i.e. modify based on what's available in telemetry item itself) then Ivan's answer is the right one.
If you'd like to add something to existing request then you need to do a few things:
1) Use Activity.Tags property bag while in a request
Activity.Current?.AddTag("TagName", "TagValue");
2) Have Telemetry initializer which puts tags as custom dimensions (in next versions we might add it as default initializer and this step will no longer be required)
/// <summary>
/// Helper class to workaround AI SDK not collecting activity tags by default.
/// This initializer allows the business logic to have no AI references.
/// </summary>
public class ActivityTagsTelemetryInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var activity = Activity.Current;
var requestTelemetry = telemetry as ISupportProperties;
if (requestTelemetry == null || activity == null) return;
foreach (var tag in activity.Tags)
{
requestTelemetry.Properties[tag.Key] = tag.Value;
}
}
}
3) Register in Startup
services.AddSingleton<ITelemetryInitializer, ActivityTagsTelemetryInitializer>();
For adding custom dimensions, you can take use of ITelemetryInitializer.
Here is an example for a .NET core web project:
1.Add a class named MyTelemetryInitializer to the project, and the code like below:
public class MyTelemetryInitializer : ITelemetryInitializer
{
public void Initialize(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
//if it's not a request, just return.
if (requestTelemetry == null) return;
if (!requestTelemetry.Properties.ContainsKey("correlationId"))
{
requestTelemetry.Properties.Add("correlationId", "id_123456");
}
}
}
2.In Startup.cs -> ConfigureServices method, use the following code:
public void ConfigureServices(IServiceCollection services)
{
//your other code
//register the class MyTelemetryInitializer.
services.AddSingleton<ITelemetryInitializer, MyTelemetryInitializer>();
}
The test result:
If you're using other programming language, please follow the official doc and use the proper method for ITelemetryInitializer.
You don't need to create your own TelemetryInitializer but can just do this from anywhere you can reference the httpContext:
var requestTelemetry = httpContext.Features.Get<RequestTelemetry>();
if (requestTelemetry != null)
{
requestTelemetry.Properties["YourCustomDimension"] = someValue;
}
Properties added in this way will be added to the requests table in Application Insights.
To add for the dependencies and traces tables you can use
System.Diagnostics.Activity.Current.AddBaggage("YourCustomDimension" someValue);
To add to traces when you write a log entry just pass in objects to the LogXXX method with a placeholder in the log message, e.g.
_logger.LogWarning("hello {YourCustomDimension}", someValue);
someValue will be serialized to json so can be a complex object if you like.
Maybe I'm missing something really simple out here but gonna ask anyways.....
I am using Xamarin forms (.NET Standard project), MVVMLight, Realm DB and ZXing Barcode Scanner.
I have a realmobject like so...
public class Participant : RealmObject
{
public string FirstName {get; set;}
public string LastName {get; set;}
public string Email {get; set;}
public string RegistrationCode {get; set;}
//More properties skipped out for brevity
}
I have the corresponding viewmodel as follows:
public class ParticipantViewModel
{
Realm RealmInstance
public ParticipantViewModel()
{
RealmInstance = Realms.Realm.GetInstance();
RefreshParticipants();
}
private async Task RefreshParticipants()
{
//I have code here that GETS the list of Participants from an API and saves to the device.
//I am using the above-defined RealmInstance to save to IQueryable<Participant> Participants
}
}
All the above works fine and I have no issues with this. In the same viewmodel, I am also able to fire up the ZXing Scanner and scan a bar code representing a RegistrationCode.
This, in turn, populates the below property (also in the viewmodel) once scanned...
private ZXing.Result result;
public ZXing.Result Result
{
get { return result; }
set { Set(() => Result, ref result, value); }
}
and calls the below method (wired up via the ScanResultCommand) to fetch the participant bearing the scanned RegistrationCode.
private async Task ScanResults()
{
if (Result != null && !String.IsNullOrWhiteSpace(Result.Text))
{
string regCode = Result.Text;
await CloseScanner();
SelectedParticipant = Participants.FirstOrDefault(p => p.RegistrationCode.Equals(regCode, StringComparison.OrdinalIgnoreCase));
if (SelectedParticipant != null)
{
//Show details for the scanned Participant with regCode
}
else
{
//Display not found message
}
}
}
I keep getting the below error....
System.Exception: Realm accessed from incorrect thread.
generated by the line below....
SelectedParticipant = Participants.FirstOrDefault(p => p.RegistrationCode.Equals(regCode, StringComparison.OrdinalIgnoreCase));
I'm not sure how this is an incorrect thread but any ideas on how I can get around to fetching the scanned participant either from the already populated IQueryable or from the Realm representation directly would be greatly appreciated.
Thanks
Yes, you're getting a realm instance in the constructor, and then using it from an async task (or thread). You can only access a realm from the thread in which you obtained the reference. Since you're only using a default instance, you should be able to simply obtain a local reference within the function (or thread) where you use it. Try using
Realm LocalInstance = Realms.Realm.GetInstance();
at the top of the function and use that. You'll need to recreate the Participants query to use the same instance as its source too. This will be the case wherever you use async tasks (threads), so either change all to get hold of the default instance on entry or reduce the number of threads that access the realm.
Incidentally I'm surprised you don't get a similar access error from within
RefreshParticipants() - maybe you're not actually accessing data via RealmInstance from there.
I'd like to return a data object that contains the details of the error with a BadRequestErrorMessageResult or BadRequestErrorMessageResult object like so:
public IHttpActionResult Action(Model model)
{
var validationResult = model.Validate();
if (validationResult.Successful)
{
// this one's okay; it supports sending data with a 200
return Ok(validationResult);
}
else
{
// However, how do I return a custom data object here
// like so?
// No such overload, I wish there was
// return BadRequest(validationResult);
}
}
The only three overloads of the ApiController.BadRequest() method are:
1. BadRequest();
2. BadRequest(string message);
3. BadRequest(ModelStateDictionary modelState);
Even with #3, a model state dictionary is ultimate a deep collection with one layer upon another, at the bottom of which, though, is a bunch of KeyValuePair<string, ModelError> where each ModelError also only has either a string or an Exception object.
Therefore, even with #3, we are only able to pack a string to send and not a custom object like I want to.
I am really not asking how I may go about working a hack or a kludge around the situation. My question is: is there an overload or another way baked into the .NET API to send an object to the client with a Bad Request HTTP status code?
I am using ASP.NET Web API version 5.2.4 targeting .NET Framework version 4.6.1.
You can use the Content<T>(...) method to do this. It returns a NegotiatedContentResult, which is serialized depending on the request headers (e.g. json, xml), and allows you to specify a HttpStatusCode.
You can use it like this:
return Content(HttpStatusCode.BadRequest, myObject);
If you wanted to, you could create your own BadRequest<T>(T obj) method in the controller as a wrapper, so then you could call it as you wanted:
public IHttpActionResult BadRequest<T>(T obj)
{
return Content(HttpStatusCode.BadRequest, obj);
}
public IHttpActionResult Action()
{
// do whatever validation here.
var validationResult = Validate();
// then return a bad request
return BadRequest(validationResult);
}
You can build/format the string in JSON format, pass it as string in the BadRequest() parameter and convert it to JSON again or any object on the caller's backend.
Haven't tried that but that should work.