A proper example of when to write a unit test

In technology, one of the things irks is people jumping on the bandwagon. Unit Test this, Agile that, Fake that, Isolate this, Dependency Inject that. Not only is it tiring, but we also find a lot of extra development times is wasted due to people trying to get a set of skills they can put on their CV.

I sound like an old hack who doesn't want to progress with technology. Quite the opposite. 

.Net Core

I wrote other posts stating .Net Core to be 75% good and 25% bad. The one thing I have been doing is migrating certain libraries and frameworks to .Net Core. Most of it seems fine BUT I still have to get my head around logging. Yes - eat that frog but when you are planning, writing code and architecting, things are a challenge.

The two things I have overcome are Unit Testing and Dependency Injection in .Net Core. I am still not 100% happy I can't use Ninject without what seems an excessive amount of configuration but I can now at least start to test my .Net Core applications.

Unit Testing

When I first saw a stub and mock, I was perplexed. Then I read "The Art of Unit Testing" and it became apparent as much care should be taken when creating base classes, stubs and fakes as the code itself. Frameworks such as Fakeiteasy, Faker and NBuilder seemed to really move things forwards but there are fundamental misassumptions regarding unit testing.

Some issues with unit testing

Explaining it is hard, but this table may help.

   Software vendor In house 
Unit testing effort  High Low
Integration testing effort  Low

High 

 We find most operational development within businesses care a lot more about the data than the technology. Vendors, care more about the technology, accuracy and calculations. This isn't to say the professionals don't care more but their customers have different demands.

In many situations, it is preferable to put more effort into integration testing because the data is what matters most. For this purpose - NBI is an awesome framework for testing data services. http://www.nbi.io/

The main issue I have with unit testing is whenever we need to stub unit tests with fake data it is likely the business semantics of the data being tested are not known at that time. Take an Interest Rate Swap unit test with a fixed versus floating schedule of payments over n periods forwards. Sure, writing the unit test may be technically correct but will it be functionally correct? Will the data be available to business users - no, because unit tests are black box operations.

Unit tests are really there to;

  • Give confidence to middle managers that a number of tests pass.
  • Let managers run statistics to explain about code coverage to prove the quality exists in the product. Naturally this is an abstraction.
  • To let release managers have some confidence on whether a release should be made.
  • To identify when a break in logic has been made which causes a test to fail.
  • To show other developers how the code works.

The only true benefit of unit tests is showing developers how the code works. The reason is simple, longer term projects on a larger team, most developers don't understand other people's unit tests and when the test results fail, nobody knows if it is important or not.

Without further ado - what is this valid use of a unit test you are talking about?

Explanation of the test first

I am building a code generator which lets me declare statements which can be applied against database metadata to generate code from database metadata. Because it is a complex piece of work, I am putting more care into writing tests to test concepts and work through ideas before committing units to the code base.

The test is to evaluate a function which returns a token boundary character type. This helps in other application objects to understand the treatment to be applied against that specific token boundary.

I use NUnit, and a number of features are noted;

  • Using the Category attribute lets me group tests by traits.
  • By categorising the test type, if I am doing continuous integration, I can run specific suites of unit tests on the build server.
  • I use Test Case attributes. This lets me put my parameters and settings at the top of the method, and tells the developer what the test is doing from a high level.

The Test

        [Category("Unit Test")]
        [TestCase("£", "$", nameof(TokenBoundaryCharacterType.BothDifferent))]
        [TestCase("£", "", nameof(TokenBoundaryCharacterType.StartOnly))]
        [TestCase("", "£", nameof(TokenBoundaryCharacterType.EndOnly))]
        [TestCase("£", "£", nameof(TokenBoundaryCharacterType.SameStartAndEnd))]
        [TestCase("", "", nameof(TokenBoundaryCharacterType.CantClassify))]
        public void TestBoundaryStartEndsAreCorrect(string Start, String End, string TokenBoundaryCharacterType)
        {
            IRuleRetriever tokenBoundaryDetector = serviceProvider.GetRequiredService<IRuleRetriever>();
            var tokenBoundaryCharacterType = TokenBoundaryCharacterType.GetEnumFromText<TokenBoundaryCharacterType>();
            var result = tokenBoundaryDetector.DetermineTokenBoundaryCharacterType(new typTokenBoundary()
            { StartCharacter = Start, EndCharacter = End });
            Assert.AreEqual(tokenBoundaryCharacterType, result);
        }

Taking this test further

Firstly, perhaps we may want to cleanse the text to avoid spaces? I would prefer to create a test object with all the test cases and save it to disk. This would do away with the many test cases. Instead, we may have something like this (pseudo code).

class BoundaryTest 

{

public string Start {get;set;}

public String End {get;set;}

public TokenBoundaryCharacterType tokenBoundaryCharacterType {get;set;}

}

We would then invoke a list object, serialise it to disk and then load it within the test. Yes, this is technically an integration test involving external dependencies but it is opening up the tests to testers and users. They can now create JSON files to test conditions. Potentially from spreadsheets.

List<BoundaryTest> tests = new List< BoundaryTest>(){new {BoundaryTest{...},...};

tests.Serialise($@"C:\Tests\{nameof(TestBoundaryStartEndsAreCorrect)}.json");

And in the test...

var tests = json.Load<BoundaryTest>(@"C:\Tests\{nameof(TestBoundaryStartEndsAreCorrect)}.json");

foreach test in tests 

{

Assert...

}

You get the idea. This may seem excessive, but it has connected users and testers with the code.

The main unit being tested

Note - this does not follow the open closed principle. I am okay with that as I am never going to have more than these types of categories.            

public TokenBoundaryCharacterType DetermineTokenBoundaryCharacterType(typTokenBoundary tokenBoundary)
        {
            TokenBoundaryCharacterType tokenBoundaryCharacterType = TokenBoundaryCharacterType.CantClassify;

            if (string.IsNullOrEmpty(tokenBoundary.StartCharacter) && !string.IsNullOrEmpty(tokenBoundary.EndCharacter))
            {
                tokenBoundaryCharacterType = TokenBoundaryCharacterType.EndOnly;
            }
            if (!string.IsNullOrEmpty(tokenBoundary.StartCharacter) && string.IsNullOrEmpty(tokenBoundary.EndCharacter))
            {
                tokenBoundaryCharacterType = TokenBoundaryCharacterType.StartOnly;
            }
            if (
                (!string.IsNullOrEmpty(tokenBoundary.StartCharacter) && !string.IsNullOrEmpty(tokenBoundary.EndCharacter)
                )
                &&
                tokenBoundary.StartCharacter == tokenBoundary.EndCharacter
                )
            {
                tokenBoundaryCharacterType = TokenBoundaryCharacterType.SameStartAndEnd;
            }

            if (
                (!string.IsNullOrEmpty(tokenBoundary.StartCharacter) && !string.IsNullOrEmpty(tokenBoundary.EndCharacter)
                )
                &&
                tokenBoundary.StartCharacter != tokenBoundary.EndCharacter
                )
            {
                tokenBoundaryCharacterType = TokenBoundaryCharacterType.BothDifferent;
            }


            return tokenBoundaryCharacterType;

        }

C#

Is the language of this post.

Add comment