I'm not dead. And I'm taking my code with me.

Its been a few weeks since my last post, so I just wanted to make sure anyone following is aware I haven't died, and neither has Codavore. But, I have had way too much going on. I'll get into that and what I've been silently completing (Unit test coverage).

Source: https://pixabay.com/photos/despaired-businessman-business-2261021/
I started a contract with Microsoft, so have 40 hours a week there. I'm interviewing for FT positions in Microsoft, Seattle and in several other countries including Spain, Canada, Denmark, France and Germany, which is easily adding about 20 more hours and calls at Midnight then 5 AM. And on top of that, my game dev group, REGameDev (Redmond Eastside Game Developers) is partnering with DigiPen to run a Game Jam. And then my locator class sprung a leak...

So amidst all that, I am still working on the project, but it has hit a spot with no visible activities. The Locator Service is an integral part of the architecture, and specifically helps to maintain the code long term. (I.e. Title - I'm not dead, and I won't let my code die either.) But since the locator class is so pivotal to the success of the project, and I found a bug in it, I have started writing a strong test suite to make sure it is and remains healthy.

In my book there are two kinds of code. Code that is isolated. It simply affects 1 tiny thing and has no potential to break other parts of the code. And then the other type is chatty code. Code that is in other codes business. Code that is depended on, and if something breaks, it can take a large portion of the project with it.  Isolated code doesn't matter as much. if it breaks, it can be fixed, replaced or removed in isolation. Something like Service Locator will be used by dozens or even hundreds of classes and making a change to 1 line of code could destroy everything.

I'll show you a function in my code, and then one of the test cases for it, while walking through what I consider strengths in its design.


        /// <summary>
        /// Sets a Location, with an immediately bound reference.
        /// </summary>
        /// <typeparam name="I">The abstraction used as a key.</typeparam>
        /// <typeparam name="T">The concrete type to return.</typeparam>
        /// <param name="content">The concrete object to be returned on later calls.</param>
        public static void Set<I, T>(T content) where T : I
        {
            var key = typeof(I);
            if (Locations.ContainsKey(key))
            {
                throw new KeyNotUniqueException("A given key was already setup earlier in the code. Please be sure that this type is only set once.", key);
            }

            var location = new Location()
            {
                AbstractType = key,
                State = Location.LocationState.HasValue,
                Value = content
            };

            Locations.Add(key, location);
        }

This is a function in my Service Locator, which is called Locator.cs, and represents the only static code that talks to other code in my program. This set method will set particular content to a type. (so I can later call Get<IStuff> and get the instance of IStuff I need.)

A standard I keep and recommend to others, is NEVER throw an exception without giving an explanation of what happened, and what the likely repair is. At the point you write the code that throws the exception, you are at the strongest for understanding the conditions in which you expect it to break. Simply throwing a KeyNotFoundException (or worse, Exception) while mildly helpful, doesn't really give much detail. And if an error is crashing your system, you can scrounge for info to resolve it, or you can just read the exception that was so clear in what the problem might be.

But I need to test this, and while I have several tests for each function, I'll show one here:


        [Test]
        public void SetIT_GivenSameITwice_ThrowsKeyNotUniqueException()
        {
            // Assume
            Locator.Set<int, int>(3);

            // Act
            try
            {
                Locator.Set<int, int>(5);
            }

            // Assert
            catch (KeyNotUniqueException)
            {
                return; // no error, this was expected.
            }

            Assert.Fail("Setting the same type twice did not throw a KeyNotUniqueException.  It should have.");
        }

This is one test case. Its in a file called LocatorTests.cs.  Which tells me the exact class its testing. Then the method name is really long. In test cases, I think that is great. The naming convention is [member]_[condition]_[result].  SetIT - refers to the method Set<I, T>(...). GivenSameITwice, indicates we are passing in the same interface twice. (Not the locator is just type to type, and I primarily refers to an intended abstraction). And finally, "ThrowsKeyNotUniqueException, where I clearly spell out the exception I expect it to throw.

Internally, I use the AAA testing pattern. SO named, NOT because of AAA game development, but because of Assume, Act and Assert. Sometimes an entire test case might fit in one line of code.  Like "SetIT_CalledOnce_ThrowsNoErrors" at which point if it throws an error, it breaks the test, and only calls the set function with 1 line. AAA all in one spot.

But the purpose is very straight forward. At first we declare our assumptions. This setups up the test to run. What are the conditions it should be at to start.  Bad programming practices would tend to trigger an assumption that make take a hundred lines or more. I.e. you have to setup so many layers, that it would indicate your code is NOT following SOLID Principals.

The next part, Act, should MOST LIKELY be 1 line. We are calling the function we said we would, or testing the member.  Finally we Assert any key conditionals to validate it happened.  If the function is supposed to write a file, you could try reading it off the file system and verifying it matches your expectations. There is even an entire Assert class loaded with special cases, such as Assert.AreEqual, when it will throw an exception if two values passed in are not matching content, or Assert.AreSame, where both variables must literally be the same object.

By adding a bunch to these, if I ever need to make a change, I can just re run my tests, and know that for the most part, my code is sound. The long test names mean that if something breaks, I should be able to know almost exactly the problem, without needing to dissect the test case's code. By testing only one condition per unit test, that makes sure my test cases are clear.  By following AAA, I'm helping to ensure I don't create speghetti code out of my tests



Comments

Popular posts from this blog

C# Parser and Code View

A Momentary Lapse of Passion

How to Setup a Multi-Scene Unity Project for Startup