Diff for Unit Tests

I recently worked on some unit tests where I wanted to verify that data was round-tripping properly. To help with this, I put together a little function that does something similar to WinDiff, but within my unit test code. It takes two input strings and does a character-by-character comparison. If it finds a difference, then it inserts a marker in the first string and returns.

The function started off really simple, but evolved into something a bit more powerful. I added support for “ignore” keywords so that the diff would skip a difference if it found a certain keyword. I also added support to attempt to “re-align” the comparison strings so that it could continue comparisons after it found something to ignore.

In my case, the strings that I am comparing are two XML strings. I use the ignore feature to skip over elements and attributes that I don’t really care about, or that I know will change.

Disclaimer: I make no guarantees that this code is 100% accurate or bug free. If you use it, you use it at your own risk.

With that said, here is the code:

        /// <summary>
        /// Locate and highlight the position of the first difference in two strings.
        /// </summary>
        /// <param name="left">The left comparison string.</param>
        /// <param name="right">The right comparison string.</param>
        /// <param name="hasSignificantDifference">Boolean indicating whether the function detected a difference that was not ignored.</param>
        /// <returns>The left string, with a marker showing the position of the detected difference.</returns>
        private string FindFirstDiff(string left, string right, out bool hasSignificantDifference, params string[] stringsToIgnore)
        {
            int iMaxLeft = left.Length - 1;
            int iMaxRight = right.Length - 1;
            int iLeft = 0;
            int iRight = 0;

            for (int i = 0; i < Math.Min(left.Length, right.Length); i++)
            {
                if (left[Math.Min(iLeft, iMaxLeft)] != right[Math.Min(iRight, iMaxRight)])
                {
                    // Get 30 characters from each string that start at the current index.
                    string leftSub = left.Substring(iLeft, Math.Min(75, left.Length - (iLeft + 1)));
                    string rightSub = right.Substring(iRight, Math.Min(75, left.Length - (iRight + 1)));

                    bool ignored = false;
                    foreach (string ignore in stringsToIgnore)
                    {
                        if (leftSub.StartsWith(ignore))
                        {
                            ignored = true;

                            bool foundMatch = false;
                            int iTriesMax = rightSub.Length - 1;
                            for (int iTries = 0; iTries < iTriesMax; iTries++)
                            {

                                // Since the left has an ignored word, see if we can find a match a bit further on.
                                for (int iOffset = 0; iOffset < (left.Length - (iLeft + 1)); iOffset++)
                                {
                                    int leftChunkStart = Math.Min(iLeft + iOffset, left.Length - 11);
                                    int leftChunkLength = Math.Min(10, left.Length - (leftChunkStart + 1));
                                    string leftChunk = left.Substring(leftChunkStart , leftChunkLength);
                                    if (rightSub.StartsWith(leftChunk))
                                    {
                                        iLeft += iOffset;
                                        foundMatch = true;
                                        break;
                                    }
                                }

                                if (foundMatch)
                                {
                                    iRight += iTries;
                                    break;
                                }
                                else
                                {
                                    // Try this the offset loop further down the right hand side.
                                    rightSub = rightSub.Substring(1);
                                }
                            }

                            break;
                        }
                        else if (rightSub.StartsWith(ignore))
                        {
                            ignored = true;

                            bool foundMatch = false;
                            int iTriesMax = leftSub.Length - 1;

                            for (int iTries = 0; iTries < iTriesMax; iTries++)
                            {

                                // Since the left has an ignored word, see if we can find a match a bit further on.
                                for (int iOffset = 0; iOffset < (right.Length - (iRight + 1)); iOffset++)
                                {
                                    int rightChunkStart = Math.Min(iRight + iOffset, right.Length - 11);
                                    int rightChunkLength = Math.Min(10, right.Length - (rightChunkStart + 1));
                                    string rightChunk = right.Substring(rightChunkStart, rightChunkLength);
                                    if (leftSub.StartsWith(rightChunk))
                                    {
                                        iRight += iOffset;
                                        foundMatch = true;
                                        break;
                                    }
                                }


                                if (foundMatch)
                                {
                                    iLeft += iTries;
                                    break;
                                }
                                else
                                {
                                    // Try this the offset loop further down the left hand side.
                                    leftSub = leftSub.Substring(1);
                                }
                            }

                            break;
                        }
                    }

                    if (!ignored)
                    {
                        hasSignificantDifference = true;
                        return left.Insert(iLeft, "----------------->");
                    }
                }

                iLeft += 1;
                iRight += 1;
            }

            hasSignificantDifference = false;
            return left;
        }

 

Here is a sample unit test that shows how I used this code.

        [TestMethod]
        public void SampleDiffTest()
        {
            string left = @"
                    <contact>
                      <code>__TEMP__</code>
                      <person>
                        <title>Mr.</title>
                        <firstName>John</firstName>
                        <middleName>Q</middleName>
                        <lastName>Smith</lastName>
                        <suffix>Jr.</suffix>
                      </person>
                      <address>
                        <street1>123 Main St.</street1>
                        <street2>Building 3</street2>
                        <street3>Unit 1</street3>
                        <apartmentNumber>1</apartmentNumber>
                        <postOfficeBox>false</postOfficeBox>
                        <city>Some City</city>
                        <state>OH</state>
                        <country>USA</country>
                        <postalCode>11111-1111</postalCode>
                      </address>
                      <alias>Work</alias>
                      <phone1>111-222-3333</phone1>
                      <phone2>222-222-2222</phone2>
                      <extension>1234</extension>
                      <fax>123-123-1234</fax>
                      <company>Some Company</company>
                      <primary>true</primary>
                    </contact>";
            string right = @"
                    <contact>
                      <code>C00000000001</code>
                      <person>
                        <title>Mr.</title>
                        <firstName>John</firstName>
                        <middleName>Q</middleName>
                        <lastName>Smith</lastName>
                        <suffix>Jr.</suffix>
                      </person>
                      <address>
                        <street1>123 Main St.</street1>
                        <street2>Building 3</street2>
                        <street3>Unit 1</street3>
                        <apartmentNumber>1</apartmentNumber>
                        <postOfficeBox>false</postOfficeBox>
                        <city>Some City</city>
                        <state>OH</state>
                        <country>USA</country>
                        <postalCode>11111-1111</postalCode>
                      </address>
                      <alias>Work</alias>
                      <phone1>111-222-3333</phone1>
                      <phone2>222-222-2222</phone2>
                      <fax>123-123-1234</fax>
                      <company>Some Company</company>
                      <primary>true</primary>
                    </contact>";
            bool hasDiff = false;
            string[] ignoreList = {"__TEMP__", "C00000"};

            TestContext.WriteLine(FindFirstDiff(left, right, out hasDiff, ignoreList));

            Assert.IsFalse(hasDiff, "The left and right strings differ.");
        }

 

Here is the output that I get in my test context. As you can see, my code is not round tripping the extension element. You can also see that it ignored the fact that the code field is different between the two samples.


                    <contact>
                      <code>__TEMP__</code>
                      <person>
                        <title>Mr.</title>
                        <firstName>John</firstName>
                        <middleName>Q</middleName>
                        <lastName>Smith</lastName>
                        <suffix>Jr.</suffix>
                      </person>
                      <address>
                        <street1>123 Main St.</street1>
                        <street2>Building 3</street2>
                        <street3>Unit 1</street3>
                        <apartmentNumber>1</apartmentNumber>
                        <postOfficeBox>false</postOfficeBox>
                        <city>Some City</city>
                        <state>OH</state>
                        <country>USA</country>
                        <postalCode>11111-1111</postalCode>
                      </address>
                      <alias>Work</alias>
                      <phone1>111-222-3333</phone1>
                      <phone2>222-222-2222</phone2>
                      <----------------->extension>1234</extension>
                      <fax>123-123-1234</fax>
                      <company>Some Company</company>
                      <primary>true</primary>
                    </contact>

 

I hope someone may find this useful. If you find any bugs or make improvements, please post in the comments.

Cheers!

 

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a website or blog at WordPress.com

Up ↑

%d bloggers like this: