Clarify Your Test Intention with ApprovalTests

In this post I’m going to explore the benefits of shorter tests. Today, I’m not interested in shortening test run times (although that’s a good thing too). Instead, I am interested in shortening the amount of code I have to read before I can figure out (or remember) the intention of a test.

If you believe, even a little bit, that "the tests are the documentation" or "the tests are the spec", then the tests better be crystal clear about what they are trying to prove. If they are not clear then the "specification" aspect of the test will be lost to future readers (possibly yourself).

So lets look at one way intention gets less clear when tests are long.

Long Test

While engaged in some practical refactoring at work, I recently came across some really long tests. The general domain was parsing, but I’ve changed the specifics to protect the guilty. I’m pasting the complete test here because I want to give you a taste of how overwhelming the initial test looked.

namespace ApprovalsExample
{
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// Describe a JSON parser.
    /// </summary>
    [TestClass]
    public class JsonParserTest
    {
        /// <summary>
        /// Parse this JSON into a POCO object.
        /// </summary>
        [TestMethod]
        public void ItConvertsJsonToPoco()
        {
            const string Source = @"{
            ""status"": ""upcoming"",
            ""visibility"": ""public"",
            ""maybe_rsvp_count"": 0,
            ""venue"": {
                ""id"": 11835602,
                ""zip"": ""92660"",
                ""lon"": -117.867828,
                ""repinned"": false,
                ""name"": ""TEKsystems"",
                ""state"": ""CA"",
                ""address_1"": ""100 Bayview Circle #3400"",
                ""lat"": 33.655819,
                ""city"": ""Newport Beach"",
                ""country"": ""us""
            },
            ""id"": ""124139172"",
            ""utc_offset"": -25200000,
            ""duration"": 10800000,
            ""time"": 1378947600000,
            ""waitlist_count"": 0,
            ""announced"": false,
            ""updated"": 1370985561000,
            ""yes_rsvp_count"": 7,
            ""created"": 1370985561000,
            ""event_url"": ""http://www.meetup.com/vNext-OrangeCounty/events/124139172/"",
            ""description"": ""<p><strong>Talk Info :</strong></p>\n<p>The techniques for building applications have changed dramatically in the last <br />\n\nfew years. Gone are the days of single-tier, battle-ship gray, boring user <br />\n\ninterfaces. Users demand that your applications (or portions) run on more than <br />\n\none device. This session will take you on a tour of how you should be architecting your application by breaking it up into services. You will learn how <br />\n\nto create your business rules and data layer as a service. This seminar will <br />\n\nassume you have some knowledge of .NET but have been developing <br />\n\napplications the old way and you are now looking to see how to use WCF and <br />\n\nthe Model-View-View-Model (MVVM) design pattern to create applications that <br />\n\ncan be run one more than one user interface platform. This session has many <br />\n\ndemonstrations and you will be led step-by-step through the code. You will walk <br />\n\naway with a sample set of services that run on Silverlight, Windows Forms, <br />\n\nWPF, Windows Phone and ASP.NET.</p>\n<p> </p>\n<p><strong>About The Speaker</strong></p>\n<p>Paul D. Sheriff is the President of PDSA, Inc. (www.pdsa.com), a Microsoft <br />\n\nPartner in Southern California. Paul acts as the Microsoft Regional Director for <br />\n\nSouthern California assisting the local Microsoft offices with several of their <br />\n\nevents each year and being an evangelist for them. Paul has authored literally <br />\n\nhundreds of books, webcasts, videos and articles on .NET, WPF, Silverlight, <br />\n\nWindows Phone and SQL Server. Paul can be reached via email at <br />\n\nPSheriff@pdsa.com. Check out Paul's new code generator 'Haystack' at <br />\n\n<a href=\""http://www.CodeHaystack.com\"">www.CodeHaystack.com</a>.</p>"",
            ""how_to_find_us"": ""Office is on the 3rd floor of the North Tower - Occupied by TekSystems"",
            ""name"": ""Paul D. Sheriff - Architecting Applications for Multiple User Interfaces"",
            ""headcount"": 0,
            ""group"": {
                ""id"": 2983232,
                ""group_lat"": 33.650001525878906,
                ""name"": ""vNext_OC"",
                ""group_lon"": -117.58999633789062,
                ""join_mode"": ""open"",
                ""urlname"": ""vNext-OrangeCounty"",
                ""who"": ""Members""
            }
        }";

            var o = Event.DeserializeJson(Source);
            const string Answer = @"Announced: False, Created: 1370985561000, Description: <p><strong>Talk Info :</strong></p>
<p>The techniques for building applications have changed dramatically in the last <br />

few years. Gone are the days of single-tier, battle-ship gray, boring user <br />

interfaces. Users demand that your applications (or portions) run on more than <br />

one device. This session will take you on a tour of how you should be architecting your application by breaking it up into services. You will learn how <br />

to create your business rules and data layer as a service. This seminar will <br />

assume you have some knowledge of .NET but have been developing <br />

applications the old way and you are now looking to see how to use WCF and <br />

the Model-View-View-Model (MVVM) design pattern to create applications that <br />

can be run one more than one user interface platform. This session has many <br />

demonstrations and you will be led step-by-step through the code. You will walk <br />

away with a sample set of services that run on Silverlight, Windows Forms, <br />

WPF, Windows Phone and ASP.NET.</p>
<p> </p>
<p><strong>About The Speaker</strong></p>
<p>Paul D. Sheriff is the President of PDSA, Inc. (www.pdsa.com), a Microsoft <br />

Partner in Southern California. Paul acts as the Microsoft Regional Director for <br />

Southern California assisting the local Microsoft offices with several of their <br />

events each year and being an evangelist for them. Paul has authored literally <br />

hundreds of books, webcasts, videos and articles on .NET, WPF, Silverlight, <br />

Windows Phone and SQL Server. Paul can be reached via email at <br />

PSheriff@pdsa.com. Check out Paul's new code generator 'Haystack' at <br />

<a href=""http://www.CodeHaystack.com"">www.CodeHaystack.com</a>.</p>, Duration: 10800000, EventUrl: , Group: ApprovalsExample.Group, HowToFindUs: , Headcount: 0, Id: 124139172, MaybeRsvpCount: 0, Name: Paul D. Sheriff - Architecting Applications for Multiple User Interfaces, Status: upcoming, Time: 1378947600000, Updated: 1370985561000, UtcOffset: 0, Venue: ApprovalsExample.Venue, Visibility: public, WaitlistCount: 0, YesRsvpCount: 0";
            Assert.AreEqual(Answer, o.ToString());
        }
    }
}

We can guess from the initial JSON blob (grabbed from vNext OC‘s meetup.com event stream) and the test name that the intention is to demonstrate something about converting JSON into .net objects. But the input data for the test is so large that we must scroll almost an entire page before seeing the first executable line of code:

var o = Event.DeserializeJson(Source);

Once we get past the first 40 or so lines, we finally see that the Event class does the parsing. Next we have 40 or so lines of expectation definition before we reach a very simple assert:

Assert.AreEqual(Answer, o.ToString());

So the test is not that hard to understand, but the signal-to-noise ratio is wimpy: 2:83. In this high level test, the specifics of the JSON source are not important. The only important thing about the source text is that it produces the expected result. Likewise the only important thing about the expected result is that it is correct and it corresponds to the provided input. So, both giant strings are noise.

Alternatives

Of course, the thrust of my argument is that ApprovalTests provides the best set of tools for cleaning up a test like this. But let me setup a couple of straw-men first.

AAA

Maybe you read the test and thought, "Fool! You didn’t follow triple-A!" While it is true that the test doesn’t conform to the Arrange/Act/Assert pattern, making it conform to AAA only yields a small improvement. By moving the call to DeserializeJson from line 42 to line 83, I now conform to the pattern:

[TestMethod]
public void ItConvertsJsonToPoco()
{
    const string Source = @"{
    /* 80 lines of "Arrange" omitted */

    var o = Event.DeserializeJson(Source);
    Assert.AreEqual(Answer, o.ToString());
}

What is the improvement? Well now all the code is next to each other, so you no longer have to hunt for the "Act" part, just skip to the bottom, and there it is. Knowing where things should be is one of the strengths of AAA, I’ll concede that. Unfortunately, we haven’t done anything to fix the signal-to-noise ratio, it is still 2:83. It’s a little easier to find the signal, because its all bunched up at the end of the noise (past 2 pages of noise now).

Move the Noise

To gain any traction on the signal to noise ratio, we need to put the noise somewhere else.

Many of testers labor under a misconception similar to this: "A test must not interact with any system except the one under test." Many usually include the file system under the category "any". Clearly, I am not a subscriber to this line of thinking, but I can take a jab at the straw-man by pointing out that the tests exist in code files that live on the file system. So, I would not worry about that, but since so many do, lets see what kind of improvement we can get by moving things around. We could promote the big strings to fields, and reduce the number of lines in our test body.

[TestMethod]
public void ItConvertsJsonToPoco()
{
    var o = Event.DeserializeJson(Source);
    Assert.AreEqual(Answer, o.ToString());
}

This certainly makes this one test look nicer. If I only consider the test method we have fantastic signal-to-noise: 1:1. This is not to say that it is absolutely clear what this test intends to prove, but we can very quickly see how it tries to prove it. So, good signal-to-noise isn’t everything, but it helps.

Can we stop here and call it a day? Of course, the truth is that you can, because the test still passes. Not surprisingly though, I say no.

I have problems with this approach. In this example, I’ve only written one test, and this solution seems to work OK, but does it scale? At work, the actual test suite contained many tests, and this solution would not scale well. Applying "move the noise" to all the tests would result in half-a-dozen "sources" and half-a-dozen "answers". These were varying lengths, some much longer than 40 lines, so we are talking about a preamble of many hundred lines of "Arrange" starting off the class before we get to any "Act" or "Assert".

I also have a problem with maintaining giant strings inside the tests, no matter where they are put in the code. First, you often run afoul of newlines and quote marks. The newlines in the answer conform to the new lines in your environment, in my case this means CRLF. The JSON blob has a mixture of line endings, so something must be done to the answer or the source to get them to match. Then we have quote marks. The JSON uses double quotes, so I had to convert them to double-double quotes to make the multi-line string literal work. Of course I could have escaped everything and used a normal interpolated string… but that’s work too. I don’t want to do any extra work.

Giant strings in the code are also very easy to mess up. If you are clumsy like me (or maybe you are perfect… but you might have someone clumsy on your team) your cursor often ends up where you least expect it when you’re in the middle of typing a tweet to your sweet old grandmother (that’s what twitter is for right?). Next thing your know, your test is failing because some how the phrase "I really liked the pie @Grandma" ends up in your giant string. I don’t like constructing my tests in such a way that debugging sessions can result from dumb mistakes.

Use ApprovalTests to Hide the Noise

ApprovalTests for .net is a assertion library that enhances your existing test framework with new capabilities for long strings, dictionaries, collections, log files, web pages, WinForm views, WPF views, Entity Framework queries, event configurations, and RDLC reports. If this is the first you’ve ever heard of ApprovalTests, then I encourage you to explore further by watching a few short videos on youtube, posted by the creator of ApprovalTests, Llewellyn Falco. Don’t let the purple hair put you off, they are great videos.

ApprovalTests provide the perfect solution for shortening the long test presented at the beginning of this post. In fact, that test’s original author had essentially re-invented approval testing without knowing it, and without gaining the power that the ApprovalTests library would provide. Our test has three parts, a known input, an action, and a known correct output. The output is the big answer string, and we know it is correct because the test passed when I inherited it from its original author. Approval testing is about capturing human intelligence, a human has declared This is what the DeserializeJson method produces. We should continue to check that the correct answer is given. An approval test automates this check.

In particular, the ApprovalTests library not only automates this check for us, but provides us with better feedback on failure. It also hides the noisy strings most of the time, but will present us with an opportunity to review or update the answer when the test fails.

At work I refactored the original test into an ApprovalTest, but for this post, I’ll just continue from where we were. I’ll post all the code so you can watch it shrink. So here is where we we want to go:

namespace ApprovalsExample
{
    using System.IO;
    using ApprovalTests;
    using ApprovalTests.Reporters;
    using ApprovalUtilities.Utilities;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// Describe a JSON parser.
    /// </summary>
    [TestClass]
    public class JsonParserTest
    {
        /// <summary>
        /// Parse this JSON into a POCO object.
        /// </summary>
        [TestMethod]
        [UseReporter(typeof(VisualStudioReporter))]
        public void ItConvertsJsonToPoco()
        {
            var text = File.ReadAllText(PathUtilities.GetAdjacentFile("sample.json"));
            var o = Event.DeserializeJson(text);
            Approvals.Verify(o);
        }
    }
}

And here is where we are after "moving the noise":

[TestClass]
public class JsonParserTest
{
    private const string Expected = @"Announced: False, Created: 1370985561000, Description: <p><strong>Talk Info :</strong></p>
<p>The techniques for building applications have changed dramatically in the last <br />

few years. Gone are the days of single-tier, battle-ship gray, boring user <br />

interfaces. Users demand that your applications (or portions) run on more than <br />

one device. This session will take you on a tour of how you should be architecting your application by breaking it up into services. You will learn how <br />

to create your business rules and data layer as a service. This seminar will <br />

assume you have some knowledge of .NET but have been developing <br />

applications the old way and you are now looking to see how to use WCF and <br />

the Model-View-View-Model (MVVM) design pattern to create applications that <br />

can be run one more than one user interface platform. This session has many <br />

demonstrations and you will be led step-by-step through the code. You will walk <br />

away with a sample set of services that run on Silverlight, Windows Forms, <br />

WPF, Windows Phone and ASP.NET.</p>
<p> </p>
<p><strong>About The Speaker</strong></p>
<p>Paul D. Sheriff is the President of PDSA, Inc. (www.pdsa.com), a Microsoft <br />

Partner in Southern California. Paul acts as the Microsoft Regional Director for <br />

Southern California assisting the local Microsoft offices with several of their <br />

events each year and being an evangelist for them. Paul has authored literally <br />

hundreds of books, webcasts, videos and articles on .NET, WPF, Silverlight, <br />

Windows Phone and SQL Server. Paul can be reached via email at <br />

PSheriff@pdsa.com. Check out Paul's new code generator 'Haystack' at <br />

<a href=""http://www.CodeHaystack.com"">www.CodeHaystack.com</a>.</p>, Duration: 10800000, EventUrl: , Group: ApprovalsExample.Group, HowToFindUs: , Headcount: 0, Id: 124139172, MaybeRsvpCount: 0, Name: Paul D. Sheriff - Architecting Applications for Multiple User Interfaces, Status: upcoming, Time: 1378947600000, Updated: 1370985561000, UtcOffset: 0, Venue: ApprovalsExample.Venue, Visibility: public, WaitlistCount: 0, YesRsvpCount: 0";

    private const string Source = @"{
        ""status"": ""upcoming"",
        ""visibility"": ""public"",
        ""maybe_rsvp_count"": 0,
        ""venue"": {
            ""id"": 11835602,
            ""zip"": ""92660"",
            ""lon"": -117.867828,
            ""repinned"": false,
            ""name"": ""TEKsystems"",
            ""state"": ""CA"",
            ""address_1"": ""100 Bayview Circle #3400"",
            ""lat"": 33.655819,
            ""city"": ""Newport Beach"",
            ""country"": ""us""
        },
        ""id"": ""124139172"",
        ""utc_offset"": -25200000,
        ""duration"": 10800000,
        ""time"": 1378947600000,
        ""waitlist_count"": 0,
        ""announced"": false,
        ""updated"": 1370985561000,
        ""yes_rsvp_count"": 7,
        ""created"": 1370985561000,
        ""event_url"": ""http://www.meetup.com/vNext-OrangeCounty/events/124139172/"",
        ""description"": ""<p><strong>Talk Info :</strong></p>\n<p>The techniques for building applications have changed dramatically in the last <br />\n\nfew years. Gone are the days of single-tier, battle-ship gray, boring user <br />\n\ninterfaces. Users demand that your applications (or portions) run on more than <br />\n\none device. This session will take you on a tour of how you should be architecting your application by breaking it up into services. You will learn how <br />\n\nto create your business rules and data layer as a service. This seminar will <br />\n\nassume you have some knowledge of .NET but have been developing <br />\n\napplications the old way and you are now looking to see how to use WCF and <br />\n\nthe Model-View-View-Model (MVVM) design pattern to create applications that <br />\n\ncan be run one more than one user interface platform. This session has many <br />\n\ndemonstrations and you will be led step-by-step through the code. You will walk <br />\n\naway with a sample set of services that run on Silverlight, Windows Forms, <br />\n\nWPF, Windows Phone and ASP.NET.</p>\n<p> </p>\n<p><strong>About The Speaker</strong></p>\n<p>Paul D. Sheriff is the President of PDSA, Inc. (www.pdsa.com), a Microsoft <br />\n\nPartner in Southern California. Paul acts as the Microsoft Regional Director for <br />\n\nSouthern California assisting the local Microsoft offices with several of their <br />\n\nevents each year and being an evangelist for them. Paul has authored literally <br />\n\nhundreds of books, webcasts, videos and articles on .NET, WPF, Silverlight, <br />\n\nWindows Phone and SQL Server. Paul can be reached via email at <br />\n\nPSheriff@pdsa.com. Check out Paul's new code generator 'Haystack' at <br />\n\n<a href=\""http://www.CodeHaystack.com\"">www.CodeHaystack.com</a>.</p>"",
        ""how_to_find_us"": ""Office is on the 3rd floor of the North Tower - Occupied by TekSystems"",
        ""name"": ""Paul D. Sheriff - Architecting Applications for Multiple User Interfaces"",
        ""headcount"": 0,
        ""group"": {
            ""id"": 2983232,
            ""group_lat"": 33.650001525878906,
            ""name"": ""vNext_OC"",
            ""group_lon"": -117.58999633789062,
            ""join_mode"": ""open"",
            ""urlname"": ""vNext-OrangeCounty"",
            ""who"": ""Members""
        }
    }";

    /// <summary>
    /// Parse this JSON into a POCO object.
    /// </summary>
    [TestMethod]
    public void ItConvertsJsonToPoco()
    {
        var o = Event.DeserializeJson(Source);
        Assert.AreEqual(Expected, o.ToString());
    }
}

Lets start refactoring.

Hide Source in File

After adding ApprovalTests to the project using nuget, I can take advantage of ApprovalUtilities to help me move the big source string into a file that sits next to the code file. I could do this by making a file and using cut and paste, but as I previously discussed, I had to mangle the source with double-double quotes to make the string literal work. I could demangle the source by hand, but letting the computer do it will be quick and less error prone.

Here’s the relevant portions of the code:

namespace ApprovalsExample
{
    using System.IO;
    using ApprovalUtilities.Utilities;

    /// <summary>
    /// Describe a JSON parser.
    /// </summary>
    [TestClass]
    public class JsonParserTest
    {
        /* Giant strings still here, omitted for clarity */

        /// <summary>
        /// Parse this JSON into a POCO object.
        /// </summary>
        [TestMethod]
        public void ItConvertsJsonToPoco()
        {
            File.WriteAllText(PathUtilities.GetAdjacentFile("sample.json"), Source);
            var o = Event.DeserializeJson(Source);
            Assert.AreEqual(Expected, o.ToString());
        }
    }
}

I added a couple of namespaces that I will need going forward, and added a line of code to write the giant source string into a file. Notice that I am still using the giant source string in the test. I’m just going to change one thing at a time as I refactor, then run the tests before making the next change. The next time I run this test, PathUtilities will provide the full path to a non-existent file next to the code file called "sample.json". Then WriteAllText will create that file by dumping the giant source string into it. So I run the test, it passes, and now I have a copy of the source in "sample.json":

{
        "status": "upcoming",
        "visibility": "public",
        "maybe_rsvp_count": 0,
        "venue": {
            "id": 11835602,
            "zip": "92660",
            "lon": -117.867828,
            "repinned": false,
            "name": "TEKsystems",
            "state": "CA",
            "address_1": "100 Bayview Circle #3400",
            "lat": 33.655819,
            "city": "Newport Beach",
            "country": "us"
        },
        "id": "124139172",
        "utc_offset": -25200000,
        "duration": 10800000,
        "time": 1378947600000,
        "waitlist_count": 0,
        "announced": false,
        "updated": 1370985561000,
        "yes_rsvp_count": 7,
        "created": 1370985561000,
        "event_url": "http://www.meetup.com/vNext-OrangeCounty/events/124139172/",
        "description": "<p><strong>Talk Info :</strong></p>\n<p>The techniques for building applications have changed dramatically in the last <br />\n\nfew years. Gone are the days of single-tier, battle-ship gray, boring user <br />\n\ninterfaces. Users demand that your applications (or portions) run on more than <br />\n\none device. This session will take you on a tour of how you should be architecting your application by breaking it up into services. You will learn how <br />\n\nto create your business rules and data layer as a service. This seminar will <br />\n\nassume you have some knowledge of .NET but have been developing <br />\n\napplications the old way and you are now looking to see how to use WCF and <br />\n\nthe Model-View-View-Model (MVVM) design pattern to create applications that <br />\n\ncan be run one more than one user interface platform. This session has many <br />\n\ndemonstrations and you will be led step-by-step through the code. You will walk <br />\n\naway with a sample set of services that run on Silverlight, Windows Forms, <br />\n\nWPF, Windows Phone and ASP.NET.</p>\n<p> </p>\n<p><strong>About The Speaker</strong></p>\n<p>Paul D. Sheriff is the President of PDSA, Inc. (www.pdsa.com), a Microsoft <br />\n\nPartner in Southern California. Paul acts as the Microsoft Regional Director for <br />\n\nSouthern California assisting the local Microsoft offices with several of their <br />\n\nevents each year and being an evangelist for them. Paul has authored literally <br />\n\nhundreds of books, webcasts, videos and articles on .NET, WPF, Silverlight, <br />\n\nWindows Phone and SQL Server. Paul can be reached via email at <br />\n\nPSheriff@pdsa.com. Check out Paul's new code generator 'Haystack' at <br />\n\n<a href=\"http://www.CodeHaystack.com\">www.CodeHaystack.com</a>.</p>",
        "how_to_find_us": "Office is on the 3rd floor of the North Tower - Occupied by TekSystems",
        "name": "Paul D. Sheriff - Architecting Applications for Multiple User Interfaces",
        "headcount": 0,
        "group": {
            "id": 2983232,
            "group_lat": 33.650001525878906,
            "name": "vNext_OC",
            "group_lon": -117.58999633789062,
            "join_mode": "open",
            "urlname": "vNext-OrangeCounty",
            "who": "Members"
        }
    }

Admittedly, the indentation is a little funky, but at least all the double-double quotes are now back to single double quotes. A trip to JSONLint shows the blob is kosher. Now I can refactor the test to use this file instead of the giant string. Only two lines need to change:

var text = File.ReadAllText(PathUtilities.GetAdjacentFile("sample.json"));
var o = Event.DeserializeJson(text);

I changed WriteAllText to ReadAllText, then captured the result in a variable. Next, I updated the call to DeserializeJson to use the text I just read, instead of the string stored in Source. I run the test and it passes.

Now my refactoring tool tells me that the Source field is unused. So I delete the giant string and run the test. It passes, leaving me with the same test, minus about 40 lines of string.

namespace ApprovalsExample
{
    using System.IO;
    using ApprovalUtilities.Utilities;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// Describe a JSON parser.
    /// </summary>
    [TestClass]
    public class JsonParserTest
    {
        private const string Expected = @"Announced: False, Created: 1370985561000, Description: <p><strong>Talk Info :</strong></p>
<p>The techniques for building applications have changed dramatically in the last <br />

few years. Gone are the days of single-tier, battle-ship gray, boring user <br />

interfaces. Users demand that your applications (or portions) run on more than <br />

one device. This session will take you on a tour of how you should be architecting your application by breaking it up into services. You will learn how <br />

to create your business rules and data layer as a service. This seminar will <br />

assume you have some knowledge of .NET but have been developing <br />

applications the old way and you are now looking to see how to use WCF and <br />

the Model-View-View-Model (MVVM) design pattern to create applications that <br />

can be run one more than one user interface platform. This session has many <br />

demonstrations and you will be led step-by-step through the code. You will walk <br />

away with a sample set of services that run on Silverlight, Windows Forms, <br />

WPF, Windows Phone and ASP.NET.</p>
<p> </p>
<p><strong>About The Speaker</strong></p>
<p>Paul D. Sheriff is the President of PDSA, Inc. (www.pdsa.com), a Microsoft <br />

Partner in Southern California. Paul acts as the Microsoft Regional Director for <br />

Southern California assisting the local Microsoft offices with several of their <br />

events each year and being an evangelist for them. Paul has authored literally <br />

hundreds of books, webcasts, videos and articles on .NET, WPF, Silverlight, <br />

Windows Phone and SQL Server. Paul can be reached via email at <br />

PSheriff@pdsa.com. Check out Paul's new code generator 'Haystack' at <br />

<a href=""http://www.CodeHaystack.com"">www.CodeHaystack.com</a>.</p>, Duration: 10800000, EventUrl: , Group: ApprovalsExample.Group, HowToFindUs: , Headcount: 0, Id: 124139172, MaybeRsvpCount: 0, Name: Paul D. Sheriff - Architecting Applications for Multiple User Interfaces, Status: upcoming, Time: 1378947600000, Updated: 1370985561000, UtcOffset: 0, Venue: ApprovalsExample.Venue, Visibility: public, WaitlistCount: 0, YesRsvpCount: 0";

        /// <summary>
        /// Parse this JSON into a POCO object.
        /// </summary>
        [TestMethod]
        public void ItConvertsJsonToPoco()
        {
            var text = File.ReadAllText(PathUtilities.GetAdjacentFile("sample.json"));
            var o = Event.DeserializeJson(text);
            Assert.AreEqual(Expected, o.ToString());
        }
    }
}

Hide Expectation in File

I could use a similar technique to hide the expectation in a file, but I don’t need to because hiding the expectation is built into the library. This is one of the tasks that ApprovalTests excels at. So, leaving all else the same, I will add a couple namespaces to the code, and make a couple small changes to the test.

namespace ApprovalsExample
{        
    using ApprovalTests;
    using ApprovalTests.Reporters;
    /* Other namespace imports remain the same */

    /// <summary>
    /// Describe a JSON parser.
    /// </summary>
    [TestClass]
    public class JsonParserTest
    {
        private const string Expected = @"Announced: False, Created: 1370985561000, Description: ..."
        /* this giant string remains here for now */

        /// <summary>
        /// Parse this JSON into a POCO object.
        /// </summary>
        [TestMethod]
        [UseReporter(typeof(VisualStudioReporter))]
        public void ItConvertsJsonToPoco()
        {
            var text = File.ReadAllText(PathUtilities.GetAdjacentFile("sample.json"));
            var o = Event.DeserializeJson(text);
            Assert.AreEqual(Expected, o.ToString());
            Approvals.Verify(o);
        }
    }
}

I run this test and it fails, but this failure now occurs after the Assert, when I make the call to Verify. This is expected behavior for ApprovalTests. Until I have approved my expected output, ApprovalTests cannot check it for me, so it must continue to fail until I give my blessing to something. Besides failing, it also gives me the opportunity to review the results by launching a reporter. In this case, the output appears in Visual Studio’s diff viewer because I specified the VisualStudioReporter when I attached the UseReporter attribute to the test method.

The output we see on the lefts side is simply the result of converting the instance o into a string. Event happens to have a decent ToString formatting method, but I could have manipulated the output by formatting or redacting the data before calling Verify. Now the only question is whether I should give this result my blessing.

In fact, its not a question at all, I know that I can immediately approve the output because the original test still passes. Although the test shows as a failure in the test runner, I can see that it failed when it reached the Approval, meaning the Assert still passed. Since the assert is checking the same output that Verify checks, then if the Assert is good, the output received by Verify must also be good. Visual Studio does not provide merging unless you are connected TFS (as far as I can tell) so my options for approval are:

  1. Select all the left side and copy/paste to the right side.
  2. Use file explorer to rename the "received" file to JsonParserTest.ItConvertsJsonToPoco.approved.txt.

I will go with option two because I don’t trust copy/paste not to make mischief with things like line-endings and character encoding.

After renaming the file, I run the test again and it passes. I should note that I normally choose to use the composite DiffReporter which searches my system for a working diff utility and uses that to show me the results. These utilities (Kdiff3, BeyondCompare, Perforce, and many more…) usually let me approve the result without resorting to renaming files. I don’t know what Microsoft thinks it is accomplishing by hobbling their diff utility in this way.

Next, I delete the original assert, re-run the test, and it passes.

/// <summary>
/// Parse this JSON into a POCO object.
/// </summary>
[TestMethod]
[UseReporter(typeof(VisualStudioReporter))]
public void ItConvertsJsonToPoco()
{
    var text = File.ReadAllText(PathUtilities.GetAdjacentFile("sample.json"));
    var o = Event.DeserializeJson(text);
    Approvals.Verify(o);
}

Now that the original Assert is gone, my refactoring tool tells me that the Expected field (formerly Answer) is unused, so I delete it, and run the test.

With the second giant string removed, I’m left with this:

namespace ApprovalsExample
{
    using System.IO;
    using ApprovalTests;
    using ApprovalTests.Reporters;
    using ApprovalUtilities.Utilities;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// Describe a JSON parser.
    /// </summary>
    [TestClass]
    public class JsonParserTest
    {
        /// <summary>
        /// Parse this JSON into a POCO object.
        /// </summary>
        [TestMethod]
        [UseReporter(typeof(VisualStudioReporter))]
        public void ItConvertsJsonToPoco()
        {
            var text = File.ReadAllText(PathUtilities.GetAdjacentFile("sample.json"));
            var o = Event.DeserializeJson(text);
            Approvals.Verify(o);
        }
    }
}

And I’ve reached my goal. If you still care about signal-to-noise ratio, its 2:3. But more importantly, the entire test, including all the kruft of namespaces, attributes and comments can be seen and understood at a glance. I would probably not spend more than a few seconds reading this test before moving on to read the actual implementation of DeserializeJson. ApprovalTests has allowed me to shorten up this test, which makes the test take up less mental real-estate, and allows me to use more of my brain thinking about the production code instead of the test.

The code for this example is available on GitHub.