When and What to Unit Test
Tags: testing, unittesting, codequality
Published: 2020-07-13 05:55:32

I have seen many posts about justifying why and why not to unit test, but most posts only talk about burden of writing unit tests, testing framework shortfalls and overhead of mocking in testing.

After writing and deciding not to write unit tests, I have finally compiled one and only one single reason why and what should be unit tested. How is not the question. If you think any testing framework is inadequate, you can simply create one.

So what should be unit tested? 

# Edge Cases


Answer is, "Edge Cases", yes, only the edge cases should at least be unit tested.

## Early Age

Lets take a simple function as an example, we will use C# to demonstrate the reason, however same can be done in any language.

```c#

   int Add(int a, int b) {
      return a + b;
   }

```

Most of you will think that this function will never fail, well hold on, see what will happen when you do `Add(int.MaxValue, int.MaxValue)`.

This is called an edge case.

If you run this code, https://dotnetfiddle.net/H3wmI6

You will see the answer given is `-2`.

Now lets assume that we wrote this simple method and which is used by 10 methods and all 10 methods are used by total of 100 another methods. So you now have 100s of dependent callers to this method.

## Middle Age

At one point, somebody decides that function returning `-2` is really not helpful, so they create a method `VerifyAdd` with following implementation.

```c#
   int VerifyAdd(int a, int b) {
       if( a == int.MaxValue || b == int.MaxValue) {
           throw new ArgumentException();
       }  
       return Add(a, b);
   }
```

And at some part of code we continue to use old `Add` and some part of code we write `VerifyAdd`.

## Mature Age

When project is couple of months old and has entered into maturity, we decide that we should change `Add` method.

```c#
   int Add(int a, int b) {
       if (a == int.MaxValue || b == int.MaxValue) {
            return -1;
       }
       return a + b;
   }
```

Now this is an issue, because half of your logic will expect `ArgumentException` and half of them will expect -1 as return answer. Leading to inconsistency in the logic.

Though this example looks simple, but when code graph is complex, when classes are subclassed and implementations are overriden, inconsistency in code keeps on rising. Now some will come and argue that this is the reason functional programming is best, nope, it is not, same can happen in any language. No language and no framework can ever provide immunity against logical inconsistencies.

# How to identify Edge cases?

1. All exceptions and errors are edge cases. So you must unit test every error, we have to make sure that the callers get correct error messages.
2. Edge cases are also evolved, as we have seen, introducing one change in logic, change introduce new edge case.

# So only edge cases for Unit tests?

Nope, Edge cases are must, arithmetic and time delay related logic must also be unit tested. 

Apart from error edge cases, we must consider logical edge cases 

1. Integer/float overflow
2. Delay, timeout, any method should not indefinitely hang
3. Never eat errors, log them and throw again, Application level unit testing must catch all errors.

# How to learn to write unit tests?

For early developers, it is difficult to understand how and when to write unit tests. Even I was against it and I never wrote in my early years thinking it was total waste of time to unit test `5 == Add(2,3)`, and it is.

To begin with, we should look at how large open source repositories are built and maintained. And look at the simple implementations and simple unit tests backing the logic.

Even if you don't contribute to any open source project, downloading them and browsing them for hours will give you many meaningful insights.