Saturday, November 19, 2022

Testing with JsonPatchDocument in Asp.Net

I've written in the past about how to use the PATCH verb for updating individual properties on larger objects in a RESTful way. When it comes time to test those controller methods, it helps to have a way to reliably setup your test data. That's what this post is about.

I created a static class that I can use from my tests to create a JsonPatchDocument to pass to your tests. Here's what that class looks like:

   1:  using Microsoft.AspNetCore.JsonPatch;
   2:  using System;
   3:  using System.Linq.Expressions;
   4:  
   5:  namespace Blog.UnitTests.Utilities.Setup
   6:  {
   7:      public static class JsonPatchDocumentSetup
   8:      {
   9:          public static JsonPatchDocument<T> BuildJsonPatchDocument<T, TProp>(Expression<Func<T, TProp>> expr, TProp newObject, string type) where T : class
  10:          {
  11:              var jsonPatchDocument = new JsonPatchDocument<T>();
  12:  
  13:              switch (type)
  14:              {
  15:                  case "add":
  16:                      jsonPatchDocument.Add(expr, newObject);
  17:                      break;
  18:                  case "copy":
  19:                      jsonPatchDocument.Copy(expr, expr);
  20:                      break;
  21:                  case "move":
  22:                      jsonPatchDocument.Move(expr, expr);
  23:                      break;
  24:                  case "remove":
  25:                      jsonPatchDocument.Remove(expr);
  26:                      break;
  27:                  case "replace":
  28:                      jsonPatchDocument.Replace(expr, newObject);
  29:                      break;
  30:              }
  31:  
  32:              return jsonPatchDocument;
  33:          }
  34:  
  35:          public static JsonPatchDocument<T> AppendToJsonPatchDocument<T, TProp>(this JsonPatchDocument<T> jsonPatchDocument, Expression<Func<T, TProp>> expr,
                                                TProp newObject, string type) where T : class
  36:          {
  37:              switch (type)
  38:              {
  39:                  case "add":
  40:                      jsonPatchDocument.Add(expr, newObject);
  41:                      break;
  42:                  case "copy":
  43:                      jsonPatchDocument.Copy(expr, expr);
  44:                      break;
  45:                  case "move":
  46:                      jsonPatchDocument.Move(expr, expr);
  47:                      break;
  48:                  case "remove":
  49:                      jsonPatchDocument.Remove(expr);
  50:                      break;
  51:                  case "replace":
  52:                      jsonPatchDocument.Replace(expr, newObject);
  53:                      break;
  54:              }
  55:  
  56:              return jsonPatchDocument;
  57:          }
  58:      }
  59:  }

And here are some examples of how you'd use it:

   1:  using Blog.BusinessLogic.Implementations;
   2:  using System.Collections.Generic;
   3:  using System.Threading.Tasks;
   4:  using Xunit;
   5:  
   6:  namespace Blog.BusinessLogic.UnitTests.Implementations
   7:  {
   8:      public class BlogClassUnitTests
   9:      {
  10:          ...test class setup...
  11:          [Theory]
  12:          [InlineData(true, true)]
  13:          [InlineData(true, false)]
  14:          [InlineData(true, null)]
  15:          [InlineData(null, true)]
  16:          [InlineData(null, false)]
  17:          [InlineData(null, null)]
  18:          [InlineData(false, true)]
  19:          [InlineData(false, false)]
  20:          [InlineData(false, null)]
  21:          public async Task DoingTheThingShouldDoWhatIsExpectedUsingInlineData(bool? firstUpdatedValue, bool? secondUpdatedValue)
  22:          {
  23:              // arrange
  24:              var jsonPatchDocument = JsonPatchDocumentSetup.BuildJsonPatchDocument<UpdateableViewModel, bool?>(p => p.FirstUpdateableProperty, firstUpdatedValue, "replace");
  25:              jsonPatchDocument.AppendToJsonPatchDocument(p => p.SecondUpdateableProperty, secondUpdatedValue, "replace");
  26:              ...additional setup...
  27:  
  28:              // act
  29:              ...take action...
  30:  
  31:              // assert
  32:              ...make assertions...
  33:          }
  34:  
  35:          public static IEnumerable<object[]> BlogTestCases
  36:          {
  37:              get
  38:              {
  39:                  return new[]
  40:                  {
  41:                      new object[]
  42:                      {
  43:                          new UpdateableViewModel
  44:                          {
  45:                              FirstUpdateableProperty = true
  46:                              NestedUpdateableProperty = new NestedUpdateableViewModel()
  47:                              SecondUpdateableProperty = false
  48:                          },
  49:                          (Expression<Func<UpdateableViewModel, NestedUpdateableViewModel>>)(p => p.NestedUpdateableProperty),
  50:                          new NestedUpdateableViewModel{Id = 987},
  51:                          new UpdateableViewModel
  52:                          {
  53:                              FirstUpdateableProperty = true
  54:                              NestedUpdateableProperty = new NestedUpdateableViewModel{Id = 987}
  55:                              SecondUpdateableProperty = false
  56:                          }
  57:                      }
  58:                  }
  59:              }
  60:          }
  61:  
  62:          [Theory, MemberData(nameof(BlogTestCases))]
  63:          public async Task DoingTheThingShouldDoWhatIsExpectedUsingMemberData<TProp1, TProp2>(UpdateableViewModel currentViewModel, Expression<Func<UpdateableViewModel, TProp1>> expression, TProp1 newObject, UpdateableViewModel expected)
  64:          {
  65:              // arrange
  66:              var jsonPatchDocument = JsonPatchDocumentSetup.BuildJsonPatchDocument&lt(expression, newObject, "replace");
  67:              ...additional setup...
  68:  
  69:              // act
  70:              ...take action...
  71:  
  72:              // assert
  73:              ...make assertions...
  74:          }
  75:      }
  76:  }

Hopefully next time I need this I remember I wrote this post, or maybe somebody else comes across this and it helps them with unit testing their own code. Maybe some day I'll turn this into a NuGet package. Ah, to dream.