Search code examples
c#asp.net-coreasp.net-core-3.0json-serialization

How to write integration tests involving DateTime Json serialization in ASP.NET Core 3.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?


Solution

  • 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).