Closed MauriceKayser closed 8 months ago
You've given two examples, and they both show a fundamental problem: we (reasonably) expect that the data should be the same both pre- and post-serialization, and it's up to you to ensure that happens.
But first, let's clear up one part of the mystery: we don't always use serialization. In the case of dotnet test
, we are running outside the IDE, so we don't serialize and de-serialize anything. The process of discovery and execution happens in the same process, so we use the original data. In the case of Test Explorer, VSTest runs discovery and execution in separate processes, and as such the data is serialized in the discovery process and deserialized in the execution process.
This is why it's critical that your data be identical pre- and post-serialization, because sometimes your data is serialized and sometimes it isn't.
So let's look at your two examples.
This one is obvious: pre-serialization, the value is "123", and post-serialization the value is "abc".
This violates our expectations. Given the description of how dotnet test
and Test Explorer differ w/rt serialization, you're getting exactly the results I would've expected.
When you call JsonSerializer.Deserialize<T>
and T
is object
, the JSON deserializer gives you back JsonElement
. This is its expected behavior. It does not convert the values back into int
and string
as you presumably expected. Prior to serialization, your Data
values are int
and string
, but after deserialization they're both JsonElement
.
Again, this violates our expectations, and again the results are exactly as I would've expected.
There is an overload of JsonSerializer.Deserialize<T>
which accepts JsonSerializerOptions
. You're calling the overload without it, which uses default options. One of those options is UnknownTypeHandling
, which is an instance of JsonUnknownTypeHandling
that indicates how deserializing of object
is handled: return JsonElement
or return JsonNode
.
There is no option to "translate the value into the closest matching CLR type", which is what it sounds like you expected it would do. You could ask for an additional flag here to do that, which would then make JsonSerializer.Deserialize<object>
do what you expected, though I presume they considered that and discarded it because the JSON doesn't carry type metadata information. In your example, you serialized an int
, but there is no pure integral type in JavaScript/JSON, only Number
. When deserializing a value like 18
, should it become an int
, float
, double
, or decimal
? At the time you asked to deserialize it, it does not have any information about what the best type to map it would be. Guessing seems inappropriate, so it counts on you to pass the correctly desired type for T
, and in the fact of object
it just gives you back the raw JsonElement
(or JsonNode
, if you so choose) to let you decide what to do with it later.
You can consider using the JsonTypeInfo
version of Deserialize
instead of Deserialize<T>
. You would save the type of the data into the serialization, then use that type during deserialization, approximately like this:
public class Property<T> : IXunitSerializable
{
public string Name { get; set; }
public T? Data { get; set; }
public Property() { Name = ""; }
public void Serialize(IXunitSerializationInfo info)
{
info.AddValue(nameof(Name), Name);
info.AddValue("DataType", (Data?.GetType() ?? typeof(T)).FullName);
info.AddValue(nameof(Data), JsonSerializer.Serialize(Data));
}
public void Deserialize(IXunitSerializationInfo info)
{
Name = info.GetValue<string>(nameof(Name));
var dataType = Type.GetType(info.GetValue<string>("DataType"));
var jsonTypeInfo = JsonTypeInfo.CreateJsonTypeInfo(dataType!, JsonSerializerOptions.Default);
Data = (T?)JsonSerializer.Deserialize(info.GetValue<string>(nameof(Data)), jsonTypeInfo);
}
public override string ToString() { return Name; }
}
1. Inconsistent usage
dotnet test
seems to use theMemberData
function directly, instead of theIXunitSerializable
implementation to get the test data, unlike the Visual Studio Test Explorer. Using an integer value as an example, the value ofentry.Data
invoid Test(TestEntry entry)
is123
when executed viadotnet test
, but0
when executed via the Visual Studio Test Explorer, as nothing is (de-)serialized in theIXunitSerializable
implementation.1. Minimal example
xUnit Test Project
with.NET 8.0 LTS
and the nameTestProject1
.Microsoft.NET.Test.Sdk
:17.8.0
xunit
:2.6.5
xunit.runner.visualstudio
:2.5.6
Replace the content of the
UnitTest1
class with:dotnet test TestProject1.sln
, the assertion does not fail (freely translated from localized version):2. Inconsistent usage
The Visual Studio Test Explorer also inconsistently uses the (de-)serialized data. The test run and tree view use the deserialized data, while the details view uses the original data from the
MemberData
function.2. Minimal example
Replace the content of the
UnitTest1
class with:Test(_: abc)
while the details view showsTest(_: 123)
.Context
I stumbled upon this, because I use
System.Text.Json.JsonSerializer
to (de-)serialize data inclass TestEntry<T> : IXunitSerializable
, whereT
can contain types like tuples, whichIXunitSerializationInfo.AddValue
does not otherwise support. That causesentry.Data
invoid Test(TestEntry<object> entry)
to be of typeSystem.Text.Json.JsonElement
in the Visual Studio Test Explorer execution, but their original, pre-JsonSerialization type in thedotnet test
execution.Reproduction steps:
Replace the content of
UnitTest1.cs
with:dotnet test TestProject1.sln
, the 1st assertion fails:I would prefer for the Visual Studio Test Explorer to behave like
dotnet test
and not expect any (de-)serialized data. Then I could get rid of theJsonSerializer
and my clunky helper function to convert data types fromJsonElement
to their actual types.