Technology12-minute read

.NET Unit Testing: Spend Upfront to Save Later On

Unit testing sometimes sounds like an unnecessary expense, but it is a powerful, yet simple tool. Unit tests allow you to identify and rectify weaknesses with relatively little effort. In this article, Toptal Software Engineer Nickolas Fisher takes you on a quick tour of unit testing and explains why you can’t have too many unit tests.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Unit testing sometimes sounds like an unnecessary expense, but it is a powerful, yet simple tool. Unit tests allow you to identify and rectify weaknesses with relatively little effort. In this article, Toptal Software Engineer Nickolas Fisher takes you on a quick tour of unit testing and explains why you can’t have too many unit tests.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Nickolas Fisher
Verified Expert in Engineering
15 Years of Experience

Nickolas specializes in large scale enterprise web apps, payment gateways, software architecture, and Windows services.

Expertise

Share

There is often a lot of confusion and doubt regarding unit testing when discussing it with stakeholders and clients. Unit testing sometimes sounds the way flossing does to a child, “I already brush my teeth, why do I need to do this?”

Suggesting unit testing often sounds like an unnecessary expense for people who consider their testing methods and user acceptance testing strong enough.

But Unit Tests are a very powerful tool and are simpler than you may think. In this article, we will take a look at unit testing and what tools are available in DotNet such as Microsoft.VisualStudio.TestTools and Moq.

We will try to build a simple class library that will calculate the nth term in the Fibonacci sequence. To do that, we will want to create a class for calculating Fibonacci sequences that depends on a custom math class that adds the numbers together. Then, we can use the .NET Testing Framework to ensure our program runs as expected.

What is Unit Testing?

Unit testing breaks the program down into the smallest bit of code, usually function-level, and ensures that the function returns the value one expects. By using a unit testing framework, the unit tests become a separate entity which can then run automated tests on the program as it is being built.

[TestClass]
    public class FibonacciTests
    {
        [TestMethod]
        //Check the first value we calculate
        public void Fibonacci_GetNthTerm_Input2_AssertResult1()
        {
            //Arrange
            int n = 2;

            //setup
            Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>();
            mockMath
                .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object);

            //Act
            int result = fibonacci.GetNthTerm(n);

            //Assert
            Assert.AreEqual(result, 1);
        }
}

A simple unit test using the Arrange, Act, Assert methodology testing that our math library can correctly add 2 + 2.

Once the unit tests are set up, if a change is made to the code, to account for an additional condition that wasn’t known when the program was first developed, for example, the unit tests will show if all cases match the expected values output by the function.

Unit testing is not integration testing. It is not end-to-end testing. While both of these are powerful methodologies, they should work in conjunction with unit testing–not as a replacement.

The Benefits and Purpose of Unit Testing

The hardest benefit of unit testing to understand, but the most important, is the ability to retest changed code on the fly. The reason it can be so hard to understand is because so many developers think to themselves, “I’ll never touch that function again,” or “I’ll just retest it when I’m done.” And stakeholders think in terms of, “If that piece is already written, why do I need to retest it?”

As someone who has been on both sides of the development spectrum, I have said both of these things. The developer inside of me knows why we have to retest it.

The changes we make on a day-to-day basis can have huge impacts. For example:

  • Does your switch properly account for a new value you put in?
  • Do you know how many times you used that switch?
  • Did you properly account for case insensitive string comparisons?
  • Are you checking for nulls appropriately?
  • Does a throw exception get handled as you expected?

Unit testing takes these questions and memorializes them in code and a process to ensure these questions are always answered. Unit tests can be run before a build to ensure that you haven’t introduced new bugs. Because unit tests are designed to be atomic they are run very quickly, usually less than 10 milliseconds per test. Even in a very large application, a full testing suite can be performed in under an hour. Can your UAT process match that?

Example of a naming convention set up to easily search for a class or method within a class to be tested.

As a developer though, maybe this just sounds like more work for you. Yes, you get peace of mind that the code your releasing is good. But unit testing also offers you the opportunity to see where your design is weak. Are you writing the same unit tests for two pieces of code? Should they be on one piece of code instead?

Getting your code to be unit testable itself is a way for you to improve your design. And for most developers who have never unit tested, or don’t take as much time to consider the design before coding, you can realize how much your design improves by making it ready for unit testing.

Is Your Code Unit Testable?

Besides DRY, we also have other considerations.

Are Your Methods or Functions Trying to do Too Much?

If you need to write overly complex unit tests that are running longer than you expect, your method may be too complicated and better suited as multiple methods.

Are You Properly Leveraging Dependency Injection?

If your method under test requires another class or function, we call this a dependency. In unit testing, we don’t care what the dependency is doing under the hood; for the purpose of the method under test, it is a black box. The dependency has its own set of unit tests that will determine if its behavior is working properly.

As a tester, you want to simulate that dependency and tell it what values to return in specific instances. This will give you greater control over your test cases. To do this, you will need to inject a dummy (or as we’ll see later, mocked) version of that dependency.

Do Your Components Interact with Each Other How You Expect?

Once you’ve worked out your dependencies and your dependency injection, you may find that you’ve introduced cyclic dependencies in your code. If Class A depends on Class B, which in turn depends on Class A, you should reconsider your design.

The Beauty of Dependency Injection

Let’s consider our Fibonacci example. Your boss tells you they have a new class that is more efficient and accurate than the current add operator available in C#.

While this particular example isn’t very likely in the real world, we do see analogous examples in other components, such as authentication, object mapping, and pretty much any algorithmic process. For the purpose of this article, let’s just pretend your client’s new add function is the latest and greatest since computers were invented.

As such, your boss hands you a black box library with a single class Math, and in that class, a single function Add. Your job of implementing a Fibonacci calculator is likely to look something like this:

 public int GetNthTerm(int n)
        {
            Math math = new Math();
            int nMinusTwoTerm = 1;
            int nMinusOneTerm = 1;
            int newTerm = 0;
            for (int i = 2; i < n; i++)
            {
                newTerm = math.Add(nMinusOneTerm, nMinusTwoTerm);
                nMinusTwoTerm = nMinusOneTerm;
                nMinusOneTerm = newTerm;
            }
            return newTerm;
        }

This isn’t horrendous. You instantiate a new Math class and use that to add the previous two terms to get the next. You run this method through your normal battery of tests, computing out to 100 terms, computing the 1000th term, the 10,000th term, and so on until you feel satisfied that your methodology works fine. Then sometime in the future, a user complains that the 501st term isn’t working as expected. You spend the evening looking through your code and trying to figure out why this corner case isn’t working. You start to get suspicious that the latest and greatest Math class isn’t quite as great as your boss thinks. But it’s a black box and you can’t really prove that–you reach an impasse internally.

The issue here is that the dependency Math isn’t injected into your Fibonacci calculator. Therefore, in your tests, you always rely on the existing, untested, and unknown results from Math to test Fibonacci against. If there is an issue with Math, then Fibonacci will always be wrong (without coding a special case for the 501st term).

The idea to correct this issue is to inject the Math class into your Fibonacci calculator. But even better, is to create an interface for the Math class that defines the public methods (in our case, Add) and implement the interface on our Math class.

 public interface IMath
    {
        int Add(int x, int y);
    }
    public class Math : IMath
    {
 public int Add(int x, int y)
        {
            //super secret implementation here
        }
    }
} 

Rather than inject the Math class into Fibonacci, we can inject the IMath interface into Fibonacci. The benefit here is that we could define our own OurMath class that we know to be accurate and test our calculator against that. Even better, using Moq we can simply define what Math.Add returns. We can define a number of sums or we can just tell Math.Add to return x + y.

        private IMath _math;
        public Fibonacci(IMath math)
        {
            _math = math;
        } 

Inject the IMath interface into the Fibonacci class

 //setup
            Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>();
            mockMath
                .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>()))
                .Returns((int x, int y) => x + y);

Using Moq to define what Math.Add returns.

Now we have a tried and true (well, if that + operator is wrong in C# we have larger issues) method for adding two numbers. Using our new Mocked IMath, we can code a unit test for our 501st term and see if we goofed our implementation or if the custom Math class needs a little more work.

Don’t Let a Method Try to Do Too Much

This example also points to the idea of a method doing too much. Sure, addition is a fairly simple operation without much need to abstract its functionality away from our GetNthTerm method. But what if the operation was a little more complicated? Instead of addition, perhaps it was model validation, calling a factory to obtain an object to operate on, or collecting additional needed data from a repository.

Most developers will try to stick to the idea of one method has one purpose. In unit testing, we try to stick to the principle that unit tests should be applied to atomic methods and by introducing too many operations to a method we make it untestable. We can often create an issue where we have to write so many tests to properly test our function.

Each parameter we add to a method increases the number of tests we have to write exponentially in accordance with the parameter’s complexity. If you add a boolean to your logic, you need to double the number of tests to write as you now need to check the true and false cases along with your current tests. In the case of model validation, the complexity of our unit tests can increase very quickly.

Diagram of the increased tests needed when a boolean is added to the logic.

We’re all guilty of adding a little extra to a method. But these larger, more complex methods create the need for too many unit tests. And it quickly becomes apparent when you write the unit tests that the method is trying to do too much. If you feel like you’re trying to test too many possible outcomes from your input parameters, consider the fact that your method needs to be broken into a series of smaller ones.

Don’t Repeat Yourself

One of our favorite tenants of programming. This one should be fairly straight forward. If you find yourself writing the same tests more than once, you have introduced code more than once. It may benefit you to refactor that work into a common class that’s accessible to both instances you are trying to use it.

What Unit Testing Tools Are Available?

DotNet offers us a very powerful unit testing platform out of the box. Using this, you can implement what is known as the Arrange, Act, Assert methodology. You arrange your initial considerations, act on those conditions with your method under test, then assert something happened. You can assert anything, making this tool even more powerful. You can assert that a method was called a specific number of times, that the method returned a specific value, that a particular type of exception was thrown, or anything else you can think of. For those looking for a more advanced framework, NUnit and its Java counterpart JUnit are viable options.

[TestMethod]

        //Test To Verify Add Never Called on the First Term
        public void Fibonacci_GetNthTerm_Input0_AssertAddNeverCalled()
        {

            //Arrange
            int n = 0;

            //setup
            Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>();
            mockMath
                .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object);

            //Act
            int result = fibonacci.GetNthTerm(n);

            //Assert
            mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Never);
        }

Testing that our Fibonacci Method handles negative numbers by throwing an exception. Unit tests can verify that the exception was thrown.

To handle dependency injection, both Ninject and Unity exist on the DotNet platform. There is very little difference between the two, and it becomes a matter of if you want to manage configurations with Fluent Syntax or XML Configuration.

For simulating the dependencies, I recommend Moq. Moq can be challenging to get your hands around, but the gist is you create a mocked version of your dependencies. Then, you tell the dependency what to return under specific conditions. For example, if you had a method named Square(int x) that squared the integer, you could tell it when x = 2, return 4. You could also tell it to return x^2 for any integer. Or you could tell it to return 5 when x = 2. Why would you perform the last case? In the event the method under the test’s role is to validate the answer from the dependency, you may want to force invalid answers to return to ensure you are properly catching the bug.

 [TestMethod]

        //Test To Verify Add Called Three times on the fifth Term
        public void Fibonacci_GetNthTerm_Input4_AssertAddCalledThreeTimes()
        {

            //Arrange
            int n = 4;

            //setup
            Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>();
            mockMath
                .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>()))
                .Returns((int x, int y) => x + y);
            UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object);

            //Act
            int result = fibonacci.GetNthTerm(n);

            //Assert
            mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Exactly(3));
        }

Using Moq to tell the mocked IMath interface how to handle Add under test. You can set explicit cases with It.Is or a range with It.IsInRange.

Unit Testing Frameworks for DotNet

Microsoft Unit Testing Framework

The Microsoft Unit Testing Framework is the out-of-the-box unit testing solution from Microsoft and included with Visual Studio. Because it comes with VS, it integrates nicely with it. When you begin a project, Visual Studio will ask you if you want to create a Unit Test Library along side of your application.

The Microsoft Unit Testing Framework also comes with a number of tools to help you better analyze your testing procedures. Also, as it is owned and written by Microsoft, there is some feeling of stability in its existence going forward.

But when working with Microsoft tools, you get what they give you. The Microsoft Unit Testing Framework can be cumbersome to integrate.

NUnit

The biggest upside for me in using NUnit is parameterized tests. In our above Fibonacci example, we can enter a number of test cases and ensure those results are true. And in the case of our 501st problem, we can always add a new parameter set to ensure that the test is always run without the need for a new test method.

The major drawback to NUnit is integrating it into Visual Studio. It lacks the bells and whistles that come with the Microsoft version and means you will need to download your own toolset.

xUnit.Net

xUnit is very popular in C# because it integrates so nicely with the existing .NET ecosystem. Nuget has many extensions of xUnit available. It also integrates nicely with Team Foundation Server, although I’m not sure how many .NET developers still use TFS over various Git implementations.

On the downside, many users complain that xUnit’s documentation is a bit lacking. For new users to unit testing, this can cause a massive headache. In addition, xUnit’s extensibility and adaptability also make the learning curve a tad steeper that NUnit or Microsoft’s Unit Testing Framework.

Test Driven Design/Development

Test driven design/development (TDD) is a bit more advanced topic that deserves its own post. However, I wanted to provide an introduction.

The idea is to start with your unit tests and tell your unit tests what is correct. Then, you can write your code around those tests. In theory, the concept sounds simple, but in practice, it’s very difficult to train your brain to think backward about the application. But the approach has the built-in benefit of not being required to write your unit tests after the fact. This leads to less refactoring, rewriting, and class confusion.

TDD has been a bit of a buzzword in recent years but adoption has been slow. Its conceptual nature is confusing for stakeholders which makes it difficult to get approved. But as a developer, I encourage you to write even a small application using the TDD approach to get used to the process.

Why You Can’t Have Too Many Unit Tests

Unit testing is one of the most powerful testing tools developers have at their disposal. It is in no way sufficient for a full test of your application, but its benefits in regression testing, code design, and documentation of purpose are unmatched.

There is no such thing as writing too many unit tests. Each edge case can propose large problems down the line in your software. Memorializing found bugs as unit tests can ensure those bugs don’t find ways to creep back into your software during later code changes. While you may add 10-20% to your project’s upfront budget, you could save much more than that in training, bug fixes, and documentation.

You can find the Bitbucket repository used in this article here.

Understanding the basics

  • What do you mean by unit testing?

    Unit testing the process of testing individual methods. Unit testing ensures that each component acts as intended.

  • Unit testing the process of testing an atomic function by passing known values into that function and asserting an expected result is produced by the function.

  • Unit testing is performed by using a framework and toolset often that is tailored towards your code base’s framework.

  • A good unit test is one that encompasses all possible inputs into your method. The results of these methods should be known to the designer and the unit test should reflect the expected result.

  • Unit tests help ensure the quality of your code by requiring you to write testable code, it ensures the functionality of your code, and it provides an excellent starting point for regression testing.

  • Yes. Unit tests may add a little to your development costs but in the long run, they will save you time and effort through regression testing and memorialization of bugs.

  • Ideally, your unit tests should cover every aspect of your code in all possibilities. Of course, in reality, that is impossible and the simple answer is you can never write too many unit tests.

Hire a Toptal expert on this topic.
Hire Now
Nickolas Fisher

Nickolas Fisher

Verified Expert in Engineering
15 Years of Experience

West Chester, PA, United States

Member since May 16, 2019

About the author

Nickolas specializes in large scale enterprise web apps, payment gateways, software architecture, and Windows services.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Expertise

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

World-class articles, delivered weekly.

By entering your email, you are agreeing to our privacy policy.

Join the Toptal® community.