Loading test data using xUnit data attributes

The xUnit framework is the preferred choice for testing .NET applications and services. The framework also provides some utilities to extend its capabilities and to implement a more maintainable testing code. It is possible to extend the DataAttribute class exposed by the xUnit.Sdk namespace to perform custom operations inside our attributes. For example, let's suppose that we create a new custom DataAttribute to load test data from a file, as follows:


namespace Catalog.API.Tests.Controllers
{
public class ItemControllerTests : IClassFixture<InMemoryApplicationFactory<Startup>>
{
...

[Theory]
[LoadData( "item")]
public async Task get_by_id_should_return_right_data(Item request)
{
var client = _factory.CreateClient();
var response = await client.GetAsync($"/api/items/{request.Id}");

response.EnsureSuccessStatusCode();

var responseContent =
await response.Content.ReadAsStringAsync();
var responseEntity = JsonConvert.DeserializeObject
<ItemResponse>(responseContent);

responseEntity.Name.ShouldBe(request.Name);
responseEntity.Description.ShouldBe(request.Description);
responseEntity.Price.Amount.ShouldBe(request.Price.Amount);
responseEntity.Price.Currency.ShouldBe(request.Price.Currency);
responseEntity.Format.ShouldBe(request.Format);
responseEntity.PictureUri.ShouldBe(request.PictureUri);
responseEntity.GenreId.ShouldBe(request.GenreId);
responseEntity.ArtistId.ShouldBe(request.ArtistId);
}

...
}
}

In this scenario, the implementation decorates the test method using the LoadData attribute, which is reading an item section from a file. Therefore, we will have a JSON file that contains all the test records, and we will use the LoadData attribute to load one of them. To customize the behavior for the ItemControllerTests class, we should create a new class and extend the DataAttribute class provided by xUnit:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit.Sdk;

namespace Catalog.Fixtures
{
public class LoadDataAttribute : DataAttribute
{
private readonly string _fileName;
private readonly string _section;
public LoadDataAttribute(string section)
{
_fileName = "record-data.json";
_section = section;
}
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
{
if (testMethod == null) throw new ArgumentNullException(nameof(testMethod));

var path = Path.IsPathRooted(_fileName)
? _fileName
: Path.GetRelativePath(Directory.GetCurrentDirectory(), _fileName);

if (!File.Exists(path)) throw new ArgumentException
($"File not found: {path}");

var fileData = File.ReadAllText(_fileName);

if (string.IsNullOrEmpty(_section)) return
JsonConvert.DeserializeObject<List<string[]>>(fileData);

var allData = JObject.Parse(fileData);
var data = allData[_section];
return new List<object[]> { new[] {
data.ToObject(testMethod.GetParameters()
.First().ParameterType
) } };
}
}
}

The LoadDataAttribute class overrides GetData(MethodInfo testMethod);, which is supplied by the DataAttribute class, and it returns the data utilized by the test methods. The implementation of the GetData method reads the content of the file defined by the _filePath attribute; it tries to serialize the content of the specified section of the file into a generic object. Finally, the implementation calls the ToObject method to convert the generic JObject into the type associated with the first parameter of the test method. The last step in the process is to create a new JSON file called record-data.json in the Catalog.API.Tests project. The file will contain the test data used by our tests:

{
"item": {
"Id": "86bff4f7-05a7-46b6-ba73-d43e2c45840f",
"Name": "DAMN.",
"Description": "DAMN. by Kendrick Lamar",
"LabelName": "TDE, Top Dawg Entertainment",
"Price": {
"Amount": 34.5,
"Currency": "EUR"
},
"PictureUri": "https://mycdn.com/pictures/45345345",
"ReleaseDate": "2017-01-01T00:00:00+00:00",
"Format": "Vinyl 33g",
"AvailableStock": 5,
"GenreId": "c04f05c0-f6ad-44d1-a400-3375bfb5dfd6",
"Genre": null,
"ArtistId": "3eb00b42-a9f0-4012-841d-70ebf3ab7474",
"Artist": null
},
"genre": {
"GenreId": "c04f05c0-f6ad-44d1-a400-3375bfb5dfd6",
"GenreDescription": "Hip-Hop"
},
"artist": {
"ArtistId": "f08a333d-30db-4dd1-b8ba-3b0473c7cdab",
"ArtistName": "Anderson Paak."
}
}

The JSON snippet has the following fields: item, artist, and genre. The fields contain data related to the test entities. Therefore, we will use them to deserialize the data into the request models and into the entity types. Consequently, we can apply the LoadData attribute to the ItemControllerTests class in the following way: 

using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Shouldly;
using Catalog.Domain.Infrastructure.Entities;
using Catalog.Domain.Requests.Item;
using Catalog.Fixtures;
using Xunit;

namespace Catalog.API.Tests.Controllers
{
public class ItemControllerTests : IClassFixture<InMemoryApplicationFactory<Startup>>
{
...

[Theory]
[LoadData("item")]
public async Task get_by_id_should_return_right_data(Item request){...}

[Theory]
[LoadData("item")]
public async Task add_should_create_new_item(AddItemRequest request){...}

[Theory]
[LoadTestData("item")]
public async Task update_should_modify_existing_item(EditItemRequest request){...}

}
}

Now, the test methods accept a request parameter of the ItemEditItemRequest, or AddItemRequest type, which will contain the data provided by the record-data.json file. Then, the object is serialized into the request parameter and sent using the HttpClient instance supplied by the InMemoryApplicationFactory:

[Theory]
[LoadData( "item")]
public async Task add_should_create_new_record(AddItemRequest request)
{
var client = _factory.CreateClient();

var httpContent = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
var response = await client.PostAsync($"/api/items", httpContent);

response.EnsureSuccessStatusCode();
response.Headers.Location.ShouldNotBeNull();
}

LoadData serializes the content defined in the record-data.json file into the AddItemRequest type. The request is then serialized as StringContent and posted using the HTTP client created by the factory. Finally, the method asserts that the resultant code is successful and the Location header is not null

We can now verify the behavior of the ItemController class by executing the dotnet test command in the root of the solution, or by running the test runner provided by our preferred IDE.

In conclusion, now we are able to define the test data in a unique central JSON file. In addition to this, we can add as much data as we want by adding new sections to the JSON file. The next part of this section will focus on improving the resilience of the APIs by adding some existence checks and handling exceptions using filters.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.79.88