0

I am writing an integration test for an Api Controller in ASP.NET Core 3.0. The test is for a route that responds with a list of entities. When I try to make the assertions on the response content, there is a divergence in the way the DateTime properties are being serialized.

I have tried using a custom JsonConverter in the test:

    public class DateTimeConverter : JsonConverter<DateTime>
    {
        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            return DateTime.Parse(reader.GetString());
        }

        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value.ToString("yyyy-MM-ddThh:mm:ss.ffffff"));
        }
    }

The problem is that this converter does not truncate trailing zeroes, while the actual response does. So, the test has a 1 in 10 chance of failing.

This is the failing test:

    [Fact]
    public async Task GetUsers()
    {
        using var clientFactory = new ApplicationFactory<Startup>();
        using var client = clientFactory.CreateClient();
        using var context = clientFactory.CreateContext();

        var user1 = context.Users.Add(new User()).Entity;
        var user2 = context.Users.Add(new User()).Entity;
        context.SaveChanges();

        var users = new List<User> { user1, user2 };
        var jsonSerializerOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };
        var serializedUsers = JsonSerializer.Serialize(users, jsonSerializerOptions);

        var response = await client.GetAsync("/users");

        var responseBody = await response.Content.ReadAsStringAsync();
        Assert.Equal(serializedUsers, responseBody);
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    }

I expected the test to pass, but I get this error instead:

  Error Message:
   Assert.Equal() Failure
                                 ↓ (pos 85)
Expected: ···1-05T22:14:13.242771-03:00","updatedAt"···
Actual:   ···1-05T22:14:13.242771","updatedAt"···

I didn't configure any serialization options in the controller's real implementation.

How can I correctly implement this integration test? Is there a straightforward way to serialize the list in the test using the same options of the real controller?

Kuteken
  • 3
  • 1
  • 1

2 Answers2

0

1. Use UTC timestamps

I really encourage you to keep the timestamps in UTC.

var x = new { UpdatedAtUtc = DateTime.UtcNow };

Console.WriteLine(JsonSerializer.Serialize(x));

produces

{"UpdatedAtUtc":"2019-11-06T02:41:45.4610928Z"}

2. Use your converter

var x = new { UpdatedAt = DateTime.Now };

JsonSerializerOptions options = new JsonSerializerOptions();
options.Converters.Add(new DateTimeConverter());

Console.WriteLine(JsonSerializer.Serialize(x, options));
{"UpdatedAt":"2019-11-06T12:50:48.711255"}

3. Use DateTimeKind.Unspecified

class X { public DateTime UpdatedAt {get;set;}}

public static void Main()
{
    var localNow = DateTime.Now;
    var x = new X{ UpdatedAt = localNow };

    Console.WriteLine(JsonSerializer.Serialize(x));
    x.UpdatedAt = DateTime.SpecifyKind(localNow, DateTimeKind.Unspecified);
    Console.WriteLine(JsonSerializer.Serialize(x));

produces

{"UpdatedAt":"2019-11-06T12:33:56.2598121+10:00"}
{"UpdatedAt":"2019-11-06T12:33:56.2598121"}

Btw. You should be using the same Json options in the tested and testing code.


Note on microseconds and DateTimeKind

As you test more you may find mismatches on timestamps between the objects that your put into the db and their equivalents retrieved from the database.

Depending on your setup DateTimes may be retrieved from the db as Local or Unspecified (even if you put Utc into the db) and you may loose some of the precision (the db column will store only what it's max resution is, it could be milliseconds).

tymtam
  • 31,798
  • 8
  • 86
  • 126
  • Thanks for your answer. You made me realise that EF Core instantiates DateTime with DateTimeKind.Unspecified, but when I instantiated new objects manually, I was using DateTimeKind.Local. I changed my constructor to `var now = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Unspecified);`, and I need no custom converters now. – Kuteken Nov 06 '19 at 03:15
0

what you want to do is to get the DateTime "Round-trip" representation, this can be done by using :

https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip

var d = DateTime.Now.ToString("o");

If you still find issues making both DateTime formats the same, then you can use "System.DateTime.Kind" property.

https://learn.microsoft.com/en-us/dotnet/api/system.datetime.kind?view=netframework-4.8

Here is a simple example I've made, you can run it online:

https://dotnetfiddle.net/ccGSEO

Mauricio Atanache
  • 2,424
  • 1
  • 13
  • 18
  • I didn't know about the "Round-trip" format, it is nice to know, thanks! The divergence was indeed due to the System.DateTime.Kind property, so there was a mismatch when comparing freshly instantiated objects (DateTimeKind.Local) vs. retrieved objects from the database (DateTimeKind.Unspecified). – Kuteken Nov 06 '19 at 03:20