Skip to content

Object Extensions

Dennis C. Mitchell edited this page Jun 21, 2019 · 12 revisions

#ObjectExtensions Class

The ObjectExtensions class provides various useful extension methods for unit testing. For example, an IsEqual method is provided to perform a deep comparison of property values of two objects of any complexity. Note that these extension methods should be confined to unit testing projects to prevent unnecessary bloat in production code. Also, these extension methods are purposely placed in their own ..Extensions namespace to prevent possible conflicts with other classes in this package.

Setup

The package can be added to your .NET Core 2 project by adding the following element to the .csproj file of a unit test class:

  <ItemGroup>
    <PackageReference Include="EDennis.NetCoreTestingUtilities" Version="3.4.0" />
    ...
  </ItemGroup>

Afterward, the package can be imported into a test class as such:

using EDennis.NetCoreTestingUtilities.Extensions;

ObjectExensions Methods

bool IsSame(T obj)

This method returns true if the provided object variable and the current object variable (this) refer to the same object in memory. The method compares hashes and property values. Example:

[Test]
public void TestSameObjectInMemory() {
    Person p1 = new Person("Bob", "Jones");
    Person p2 = p1; //assign p2 to point to p1 (same object in memory)
    Assert.True(p1.IsSame(p2)); 
}

bool IsEqual(T obj)

This method returns true if the provided object variable and the current object variable (this) have the same property values. NOTE: this is a deep comparison. Example:

[Test]
public void TestEqualObjects() {
    Person p1 = new Person("Bob", "Jones");
    Person p2 = new Person("Bob", "Jones");
    Assert.False(p1.IsSame(p2));
    Assert.True(p1.IsEqual(p2));
}

bool IsEqual(T obj, string[] pathsToIgnore)

This method returns true if the provided object variable and the current object variable (this) have the same property values, ignoring all properties at the provided JSON paths. Example:

[Test]
public void TestEqualObjectsIgnoringSelectedProperties() {
    Person p1 = new Person(1, "Bob", "Jones");
    Person p2 = new Person(2, "Bob", "Jones");
    Assert.False(p1.IsEqual(p2));
    Assert.True(p1.IsEqual(p2, new string[] { "ID" })); //ignore ID
}

string ToJsonString()

This method serializes an object to a JSON string. Example:

[Test]
public void TestToJsonString() {
    string expectedJson =
        @"{
         ""ID"": 0,
         ""FirstName"": ""David"",
         ""LastName"": ""Parks""
        }";
    expectedJson = JToken.Parse(expectedJson).ToString(Formatting.Indented);
    Person p1 = new Person("David", "Parks");
    string actualJson = p1.ToJsonString();
    Assert.AreEqual(expectedJson, actualJson);
}

T FromJsonString(string json)

This method creates a new object of type T by deserializing the provided JSON string. When called from an object constructor, this method provides a convenient way to load an object with properties. Example:

[Test]
public void TestFromJsonString() {
    string json =
        @"{
         ""FirstName"": ""David"",
         ""LastName"": ""Parks""
        }";
    var p1 = new Person().FromJsonString(json);
    Person p2 = new Person("David", "Parks");
    Assert.True(p1.IsEqual(p2));
}

T FromJsonPath(string filePath, string objectPath)

This method creates a new object of type T by deserializing a JSON object or array in the provided JSON file at the provided path. When called from an object constructor, this method provides a convenient way to load an object with properties. NOTE: you can use "" or "/" instead of "." in the objectPath. Example:

[Test]
public void TestFromJsonPath2() {
    var p1 = new Person().FromJsonPath(@"DavidParks.json", "Contact");
    Person p2 = new Person("Jill", "Parks");
    Assert.True(p1.IsEqual(p2));
}

T FromJsonPath(string jsonFileObjectPath)

This method is equivalent to the above method, but it combines the filePath and objectPath into a single expression. Note that you can use a backslash or forward slash instead of period in the objectPath. Example:

[Test]
public void TestFromJsonPath3() {
    var p1 = new Person().FromJsonPath(@"DavidParks.json\Contact");
    Person p2 = new Person("Jill", "Parks");
    Assert.True(p1.IsEqual(p2));
}

T FromJsonPath(JToken jtoken, string jsonPath)

This method creates a new object of type T by deserializing a JSON object or array at the provided path in the provided JToken object. When called from an object constructor, this method provides a convenient way to load an object with properties. Example:

[Test]
public void TestFromJsonPath1() {
    string json =
       @"{
         ""FirstName"": ""David"",
         ""LastName"": ""Parks"",
         ""Contact"": {
             ""FirstName"": ""Jill"",
             ""LastName"": ""Parks""
            }
        }";
    var jtoken = JToken.Parse(json);
    var p1 = new Person().FromJsonPath(jtoken, "Contact");
    Person p2 = new Person("Jill", "Parks");
    Assert.True(p1.IsEqual(p2));
}

T FromSql(string jsonPath, string sqlForJsonFile, DbContext context)

This method creates a new object of type T by deserializing a JSON object or array from a SQL Server FOR JSON query. When called from an object constructor, this method provides a convenient way to load an object with properties. Example:

[Test]
public void TestFromSql1() {
    using (var context = new JsonResultContext()) {
        var expected = new List<Person>().FromJsonPath(@"01.json\persons");
        var actual = new List<Person>().FromSql(@"01.sql", context);
        Assert.True(actual.IsEqual(expected));
    } 
}
{
  "_comment": "01.json"
  "persons": [
    {
      "ID": 1,
      "FirstName": "Bob",
      "LastName": "Jones",
      "DateOfBirth": "1980-01-23T00:00:00",
      "Skills": [
        {
          "Category": "Application Development",
          "Score": 3
        },
        {
          "Category": "Project Management",
          "Score": 3
        }
      ]
    },
    {
      "ID": 2,
      "FirstName": "Jill",
      "LastName": "Jones",
      "DateOfBirth": "1981-01-24T00:00:00",
      "Skills": [
        {
          "Category": "Application Development",
          "Score": 2
        },
        {
          "Category": "Project Management",
          "Score": 1
        }
      ]
    }
  ]
}
-- 01.sql
declare @Person table (
	ID int,
	FirstName varchar(30),
	LastName varchar(30),
	DateOfBirth datetime
);
declare @Skill table (
	PersonID int,
	Category varchar(30),
	Score int
);

insert into @Person(ID,FirstName,LastName,DateOfBirth)
	values
		(1,'Bob','Jones','1980-01-23'),
		(2,'Jill','Jones','1981-01-24');

insert into @Skill(PersonID,Category,Score)
	values
		(1,'Application Development',3),
		(1,'Project Management',3),
		(2,'Application Development',2),
		(2,'Project Management',1);

declare @j varchar(max);
	set @j =
	(
		select ID,FirstName,LastName,DateOfBirth,
			(select  
				Category,Score
				from @Skill s
				where s.PersonID = p.ID
				for json path) Skills
			from @Person p
			for json path
	);

select @j [json];

T FromSql(string jsonPath, string sqlForJsonFile, string connectionString)

This method creates a new object of type T by deserializing a JSON object or array from a SQL Server FOR JSON query. When called from an object constructor, this method provides a convenient way to load an object with properties. Example:

[Test]
public void TestFromSql1() {
    using (var context = new JsonResultContext()) {
        var expected = new List<Person>().FromJsonPath(@"01.json\persons");
        var actual = new List<Person>().FromSql(@"01.sql",
                "Server=(localdb)\\mssqllocaldb;Database=tempdb;Trusted_Connection=True;");
        Assert.True(actual.IsEqual(actual));
    } 
}

Xunit Helper Methods

IsEqualOrWrite -- compares actual and expected objects to each other and, when they are unequal, writes out the two objects side-by-side, highlighting the discrepant properties.

Parameters

Name Required? Description
T obj2 Yes The object to compare (typically the expected value)
int maxDepth No The maximum depth of the object graph to compare†
string[] propertiesToIgnore No a string array of property names to ignore during the comparison
ITestOutputHelper output Yes A reference to Xunit's injected ITestOutputHelper
bool No Whether to ignore the order of elements in arrays during the comparison
†Special note: for arrays, count the array itself as the first level

Example

//compare actual object to expected object, and
//write out the objects side-by-side when they are not equal
Assert.True(actual.IsEqualOrWrite(expected, 3, Output)); 

ActionResultExtensions and IActionResultExtensions

One of the most powerful features of .Net MVC is the ability to test controller actions (methods) directly without launching a web server and making an HTTP request. Unfortunately, if the return type of the controller action is an IActionResult or ActionResult, comparing the actual return type to an expected return type requires a little extra code.

The extension methods provided by the current library make the testing process a little easier by providing extension methods that extract the object value and status code of the ActionResult or IActionResult. There isn't any magical code here, but the extension methods can reduce the testing code a little.

The ActionResultExtensions class and the IActionResultExtensions class provide extension methods that perform the same functionality:

  1. GetObject Method -- returns the object contained in the response body
  2. GetStatusCode Method -- returns the status code from an action result or 0 if there is no status code