Unit Tests are not an Option!

Introduction

I’ve been writing software since 1978.  Which is to say that I’ve seen many paradigm changes.  I witnessed the inception of object oriented programming.  I first became aware of objects when I read a Byte magazine article on a language called SmallTalk (issued August 1981).  I read and re-read that article many times to try and understand what the purpose of object oriented programming was.  Object oriented programming took ten more years before programmers began to recognize it.  In the early 90’s the University of Michigan only taught a few classes using object oriented C++.  It was still new and shiny.  Now all languages are object oriented, or they are legacy languages.

The web was another major paradigm that I witnessed.  Before the browser was invented (while I was in college), all programs were written to be executed on the machine it was run on.  I was immersed in the technology of the Internet while I was a student at UofM and we used tools such as Telnet, FTP, Archie, DNS, and Gopher (to name a few), to navigate and find information.  The Internet was primarily composed of data about programming.  When Mosaic came along as well as HTML, the programming world went crazy.  The technology didn’t mature until the early 2000’s.  Many programming languages were thrown together to accommodate the new infrastructure (I’m looking at you “Classic ASP”).

Extreme programming came of age in the late 90’s.  I did not get involved in XP until the mid 2000’s.  Waterfall was the way things were done.  The industry was struggling with automated testing suites.  Unit testing came onto the scene, but breaking dependencies was an unknown quantity.  It took some years before somebody came up with the idea of inversion of control.  The idea was so abstract that most programmers ignored it and moved on.

The latest paradigm change, and it’s much bigger than most will give it credit for is the IOC container.  Even Microsoft has incorporated this technology into their latest tools.  IOC is part of .Net Core.  If you’re a programmer and you haven’t used IOC containers yet, or you don’t understand the underlying reason for it, you better get on the bandwagon.  I predict that within five years, IOC will be recognized as the industry standard, even for companies that build software for their internal use only.  It will be difficult to get a job as a programmer without understanding this technology.  Don’t believe me?  Pretend you’re a software engineer with no object oriented knowledge.  Now search for a job and see what results come up.  Grim, isn’t it?

Where am I going with this?  I currently work for a company that builds medical software.  We have a huge collection of legacy code.  I’m too embarrassed to admit how large this beast is.  It just makes me cry.  Our company uses the latest tools and we have advanced developers who know how to build IOC containers, unit tests, properly scoped objects, etc.  We also practice XP, to a limited degree.  We do the SCRUMs, stand-ups, code-reviews (sometimes), demos, and sprint planning meetings.  What we don’t do is unit testing.  Oh we have unit tests, but the company mandate is that they are optional.  When there is extra time to build software, unit tests are incorporated.  Only a small hand-full of developers incorporate unit tests into their software development process.  Even I have built some new software without unit tests (and I’ve paid the price).

The bottom line is that unit tests are not part of the software construction process.  The company is staffed with programmers that are unfamiliar with TDD (Test Driven Development) and in fact, most are unfamiliar with unit testing altogether.  Every developer has heard of unit test, but I suspect that many are not sold on the concept of the unit test.  Many developers look at unit testing as just more work.  There are the usual arguments against unit testing: They need to be maintained, they become obsolete, they break when I refactor code, etc.  These are old arguments that were disproved years ago, but, like myths, they get perpetuated forever.

I’m going to divert my the subject a bit here, just to show how crazy this is.

Our senior developers have gathered in many meetings to discuss the agreed upon architecture that we are aiming for.  That architecture is not much different from any other company: Break our monolithic application into smaller APIs, use IOC containers, separate database concerns from business and business from the front-end.  We have a couple dozen APIs and they were written with this architecture in mind.  They are all written with IOC containers.  We use Autofac for our .Net applications and .Net Core has it’s own IOC container technology.  Some of these APIs have unit tests.  These tests were primarily added after the code was written, which is OK.  Some of our APIs have no unit tests.  This is not OK.

So the big question is: Why go through the effort of using an IOC container in the first place, if there is no plan for unit tests?

The answer is usually “to break dependencies.”  Which is correct, except, why?  Why did anybody care about breaking dependencies?  Just breaking dependencies gains nothing.  The IOC container itself, does not help with the maintainability of the code.  Is it safer to refactor code with an IOC container?  No.  Is it easier to troubleshoot and fix bugs in code that has dependencies broken?  Not unless your using unit tests.

My only conclusion to this crazy behavior is that developers don’t understand the actual purpose of unit testing.

Unit Test are Part of the Development Process

The most difficult part of creating unit tests is breaking dependencies.  IOC containers make that a snap.  Every object (with some exceptions) should be put into the container.  If an object instance must be created by another object, then it must be created inside the IOC container.  This will break the dependency for you.  Now unit testing is easy.  Just focus on one object at a time and write tests for that object.  If the object needs other objects to be injected, then us a mocking framework to make mock those objects.

As a programmer, you’ll need to go farther than this.  If you want to build code that can be maintained, you’ll need to build your unit tests first or at least concurrently.  You cannot run through like the Tasmanian devil, building your code, and then follow-up with a hand-full of unit tests.  You might think you’re clever by using a coverage tool to make sure you have full code coverage, but I’m going to show an example where code-coverage is not the only reason for unit testing.  Your workflow must change.  At first, it will slow you down, like learning a new language.  Keep working at it and eventually, you don’t have to think about the process.  You just know.

I can tell you from experience, that I don’t even think about how I’m going to build a unit test.  I just look at what I’ll need to test and I know what I need to do.  It took me years to get to this point, but I can say, hands down, that unit testing makes my workflow faster.  Why?  Because I don’t have to run the program in order to test for all the edge cases.  I write one unit test at a time and I run that test against my object.  I use unit testing as a harness for my objects.  That is the whole point of using an IOC container.  First, you take care of the dependencies, then you focus on one object at a time.

Example

I’m sure you’re riveted by my rambling prose, but I’m going to prove what I’m talking about.  At least on a small scale.  Maybe this will change your mind, maybe it won’t.

Let’s say for example, I was writing some sort of API that needed to return a set of patient records from the database.  One of the requirements is that the calling program can feed filter parameters to select a date range for the records desired.  There is a start date and an end date filter parameter.  Furthermore, each date parameter can be null.  If both are null, then give me all records.  If the start parameter is null, then give me up to the end date.  If the end date is null, then give me from the start date to the latest record.  The data in the database will return a date when the patient saw the doctor.  This is hypothetical, but based on a real program that my company uses.  I’m sure this scenario is used by any company that queries a database for web use, so I’m going to use it.

Let’s say the program is progressing like this:

public class PatienData
{
  private DataContext _context;

  public List<PatientVisit> GetData(int patientId, DateTime ? startDate, DateTime ? endDate)
  {
    var filterResults = _context.PatientVisits.Where(x => x.BetweenDates(startDate,endDate));

    return filterResults.ToList();
  }
}

You don’t want to include the date range logic in your LINQ query, so you create an extension method to handle that part.  Your next task is to write the ugly code called “BetweenDates()”.  This will be a static extension class that will be used with any of your PatientVisit POCOs.  If you’re unfamiliar with a POCO (Plain Old C# Code) object, then here’s a simple example:

public class PatientVisit
{
  public int PatientId { get; set; }
  public DateTime VisitDate { get; set; }
}

This is used by Entity Framework in a context.  If you’re still confused, please search through my blog for Entity Framework subjects and get acquainted with the technique.

Back to the “BetweenDates()” method.  Here’s the empty shell of what needs to be written:

public static class PatientVisitHelpers
{
  public static bool BetweenDates(this PatientVisit patientVisit, DateTime ? startDate, DateTime ? endDate)
  {
    
  }
}

Before you start to put logic into this method, start thinking about all the edge cases that you will be required to test.  If you run in like tribe of Comanche Indians and throw the logic into this method, you’ll be left with a manual testing job that will probably take you half a day (assuming you’re thorough).  Later, down the road, if someone discovers a bug, you will need to fix this method and then redo all the manual tests.

Here’s where unit test are going to make your job easy.  The unit tests are going to be part of the discovery process.  What Discovery?  One aspect of writing software that is different from any other engineering subject is that every project is new.  We don’t know what has to be built until we start to build it.  Then we “discover” aspects of the problem that we never anticipated before.  In this sample, I’ll show how that occurs.

Let’s list the rules:

  1. If the dates are both null, give me all records.
  2. If the first date is null, give me all records up to that date (including the date).
  3. If the last date is null, give me all records from the starting date (including the start date).
  4. If both dates exist, then give me all records, including the start and end dates.

According to this list, there should be at least four unit tests.  If you discover any edge cases, you’ll need a unit tests for each edge case.  If a bug is discovered, you’ll need to add a unit test to simulate the bug and then fix the bug.  Which tells you that you’ll keep adding unit tests to a project every time you fix a bug or add a feature (with the exception that one or more unit tests were incorrect in the first place).  An incorrect unit test usually occurs when you misinterpret the requirements.  In such an instance, you’ll fix the unit test and then fix your code.

Now that we have determined that we need five unit tests, create five empty unit test methods:

public class PatientVisitBetweenDates
{
  [Fact]
  public void BothDatesAreNull()
  {

  }
  [Fact]
  public void StartDateIsNull()
  {

  }
  [Fact]
  public void EndDateIsNull()
  {

  }
  [Fact]
  public void BothDatesPresent()
  {

  }
}

I have left out the IOC container code from my examples.  I am testing a static object that has no dependencies, therefore, it does not need to go into a container.  Once you have established an IOC container and you have broken dependencies on all objects, you can focus on your code just like the samples I am showing here.

Now for the next step: Write the unit tests.  You already have the method stubbed out.  So you can complete your unit tests first and then write the code to make the tests pass.  You can do one unit test, followed by writing code, then the next test, etc.  Another method is to write all the unit tests and then write the code to pass all tests.  I’m going to write all the unit tests first.  By now, you might have analyzed my empty unit tests and realized what I meant earlier by “discovery”.  If you haven’t, then this will be a good lesson.

For the first test, we’ll need the setup data.  We don’t have to concern ourselves with any of the Entity Framework code other than the POCO itself.  In fact, the “BetweenDates()” method only looks at one instance, or rather, one record.  If the date of the record will be returned with the set, then the method will return true.  Otherwise, it should return false.  The tiny scope of this method makes our unit testing easy.  So put one record of data in:

[Fact]
public void BothDatesAreNull()
{
  var testSample = new PatientVisit
  {
    PatientId = 1,
    VisitDate = DateTime.Parse("1/7/2015")
  };
}

Next, setup the object and perform an assert.  This unit test should return a true for the data given because both the start date and the end date passed into our method will be null and we return all records.

[Fact]
public void BothDatesAreNull()
{
  var testSample = new PatientVisit
  {
    PatientId = 1,
    VisitDate = DateTime.Parse("1/7/2015")
  };

  var result = testSample.BetweenDates(null,null);
  Assert.True(result);
}

This test doesn’t reveal anything yet.  Technically, you can put code into your method that just returns true, and this test will pass.  At this point, it would be valid to do so.  Then you can write your next test and then refactor to return the correct value.  This would be the method used for pure Test Driven Development.  Only use the simplest code to make the test pass.  The code will be completed when all unit tests are completed and they all pass.

I’m going to go on to the next unit test, since I know that the first unit test is a trivial case.  Let’s use the same data we used on the last unit test:

[Fact]
public void StartDateIsNull()
{
  var testSample = new PatientVisit
  {
    PatientId = 1,
    VisitDate = DateTime.Parse("1/7/2015")
  };
}

Did you “discover” anything yet?  If not, then go ahead and put the method setup in:

[Fact]
public void StartDateIsNull()
{
  var testSample = new PatientVisit
  {
    PatientId = 1,
    VisitDate = DateTime.Parse("1/7/2015")
  };
  
  var result = testSample.BetweenDates(null, DateTime.Parse("1/8/2015"));
}

Now, you’re probably scratching your head because we need at least two test cases and probably three.  Here are the tests cases we need when the start date is null but the end date is filled in:

  1. Return true if the visit date is before the end date.
  2. Return false if the visit date is after the end date.

What if the date is equal to the end date?  Maybe we should test for that edge case as well.  Break the “StartDateIsNull()” unit test into three unit tests:

[Fact]
public void StartDateIsNullVisitDateIsBefore()
{
  var testSample = new PatientVisit
  {
    PatientId = 1,
    VisitDate = DateTime.Parse("1/7/2015")
  };
  var result = testSample.BetweenDates(null, DateTime.Parse("1/3/2015"));
  Assert.True(result);
}
[Fact]
public void StartDateIsNullVisitDateIsAfter()
{
  var testSample = new PatientVisit
  {
    PatientId = 1,
    VisitDate = DateTime.Parse("1/7/2015")
  };
  var result = testSample.BetweenDates(null, DateTime.Parse("1/8/2015"));
  Assert.False(result);
}
[Fact]
public void StartDateIsNullVisitDateIsEqual()
{
  var testSample = new PatientVisit
  {
    PatientId = 1,
    VisitDate = DateTime.Parse("1/7/2015")
  };
  var result = testSample.BetweenDates(null, DateTime.Parse("1/7/2015"));
  Assert.True(result);
}

Now you can begin to see the power of unit testing.  Would you have manually tested all three cases?  Maybe.

That also reveals that we will be required to expand the other two tests that contain dates.  The test case where we have a null end date will have a similar set of three unit tests and the in-between dates test will have more tests.  For the in-between, we now need:

  1. Visit date is less than start date.
  2. Visit date is greater than start date but less than end date.
  3. Visit date is greater than end date.
  4. Visit date is equal to start date.
  5. Visit date is equal to end date.
  6. Visit date is equal to both start and end date (start and end are equal).

That makes 6 unit test for the in-between case.  Bringing our total to 13 tests.

Fill in the code for the remaining tests.  When that is completed, verify each test to make sure they are all valid cases.  Once this is complete, you can write your code for the helper method.  You now have a complete detailed specification for your method written in unit tests.

Was that difficult?  Not really.  Most unit tests fall into this category.  Sometimes you’ll need to mock an object that your object under test depends on.  That is made easy by the IOC container.

Also, you can execute your code directly from the unit test.  Instead of writing a test program to send inputs to your API, or using your API in a full system where you are typing data in manually, you just execute the unit test you are working with.  You type in your code, then run all the unit tests for this method.  As you create code to account for each test case, you’ll see your unit tests start to turn green.  When all unit tests are green, you’re work is done.

Now, if Quality finds a bug that leads to this method, you can reverify your unit tests for the case that QA found.  You might discover a bug in code that is outside your method or it could have been a case missed by your unit tests.  Once you have fixed the bug, you can re-run the unit tests instead of manually testing each case.  In the long run, this will save you time.

Code Coverage

You should strive for 100% code coverage.  You’ll never get it, but the more code you can cover, the safer it will be to refactor code in the future.  Any code not covered by unit tests is at risk for failure when code is refactored.  As I mentioned earlier, code coverage doesn’t solve all your problems.  In fact, if I wrote the helper code for the previous example and then I created unit tests afterwards, I bet I can create two or three unit tests that covers 100% of the code in the helper method.  What I might not cover are edge cases, like the visit date equal to the start date.  It’s best to use code coverage tools after the code and unit tests are written.  The code coverge will be your verfication that you didn’t miss something.

Another problem with code coverage tools is that it can make you lazy.  You can easily look at the code and then come up with a unit test that executes the code inside an “if” statement and then create a unit test to execute code inside the “else” part.  The unit tests might not be valid.  You need to understand the purpose of the “if” and “else” and the purpose of the code itself.  Keep that in mind.  If you are writing new code, create the unit test first or concurrently.  Only use the code coverage tool after all your tests pass to verify you covered all of your code.

Back to the 20,000 foot View

Let’s take a step back and talk about what the purpose of the exercise was.  If you’re a hold-out for a world of programming without unit tests, then you’re still skeptical of what was gained by performing all that extra work.  There is extra code.  It took time to write that code.  Now there are thirteen extra methods that must be maintained going forward.

Let’s pretend this code was written five years ago and it’s been humming along for quite some time without any bugs being detected.  Now some entry-level developer comes on the scene and he/she decides to modify this code.  Maybe the developer in question thinks that tweaking this method is an easy short-cut to creating some enhancement that was demanded by the customer.  If the code is changed and none of the unit tests break, then we’re OK.  If the code is changed and one or more unit tests breaks, then the programmer modifying the code must look at those unit tests and determine if the individual behavoirs should be changed, or maybe those were broken because the change is not correct.  If the unit tests don’t exist, the programmer modifying the code has no idea what thought process and/or testing went into the original design.  The programmer probably doesn’t know the full specification of the code when it was written.  The suite of unit tests make the purpose unambiguous.  Any programmer can look at the unit tests and see exactly what the specification is for the method under test.

What if a bug is found and all unit tests pass?  What you have discovered is an edge case that was not known at the time the method was written.  Before fixing the bug, the programmer must create a unit test with the edge case that causes the bug.  That unit test must fail with the current code and it should fail in the same manner as the real bug.  Once the failing unit test is created, then the bug should be fixed to make the unit test pass.  Once that has been accomplished, run all unit tests and make sure you didn’t break prevous features when fixing the bug.  This method ends the whack-a-mole technique of trying to fix bugs in software.

Next, try to visualize a future where all your business code is covered by unit tests.  If each class and method had a level of unit testing to the specification that this mehod has, it would be safe and easy to refactor code.  Any refactor that breaks code down the line will show up in the unit tests (as broken tests).  Adding enhancements would be easy and quick.  You would be virtually guarenteed to produce a quality product after adding an enhancement.  That’s because you are designing the software to be maintainable.

Not all code can be covered by unit tests.  In my view, this is a shame.  Unfortunately, there are sections of code that cannot be put into a unit test for some reason or another.  With an IOC container, your projects should be divided into projects that are unit testable and projects that are not.  Projects, such as the project containiner your Entity Framework repository, are not unit testable.  That’s OK, and you should limit how much actual code exists in this project.  All the code should be POCO’s and some connecting code.  Your web interface should be limited to code that connects the outside world to your business classes.  Any code that is outside the realm of unit testing is going to be difficult to test.  Try to limit the complexity of this code.

Finally…

I have looked over the shoulder of students building software for school projects at the University of Maryland and I noticed that they incorporated unit testing into a Java project.  That made me smile.  While the project did not contain an IOC container, it’s a step in the right direction.  Hopefully, withing the next few years, universities will begin to produce students that understand that unit tests are necessary.  There is still a large gap between those students and those in the industry that have never used unit tests.  That gap must be filled in a self-taught manner.  If you are one of the many who don’t incorporate unit testing into your software development process, then you better start doing it.  Now is the time to learn and get good at it.  If you wait too long, you’ll be one of those Cobol developers that wondered who moved their cheese.

 

Leave a Reply