Given that one goal of test-driven development (TDD) is never to touch a debugger, it seems like an oxymoron to debug a unit test. But let's be honest, it happens! One day you're humming along nicely with your tests. The next day, you've got failing tests like the ones described in Peter Morlion's post, only you don't know why. So you've got to debug the unit test.
As you'll see in this post, debugging unit tests isn't so different from debugging any code. You can debug a unit test in Visual Studio by following this simple guide. Let's start by setting up an example and walking through it.
Example Case
I'm going to build out an example so you can follow along. Please, feel free to copy this source code locally. Doing is learning, after all! Here's the gist of what we're going to do:
1. We'll unit test a method that computes the future value of an annuity. Your retirement account is one kind of annuity. You invest CF (cash flow) dollars per year and earn i interest each year, compounded annually. Your account will grow to FVA (future value of annuity) after n years.
2. We'll need to test a few conditions against expected values; the execution of the function is the same for each case. We'll use xUnit to write data-driven unit tests.
3. Once we've written the tests, we'll start writing the code. The code won't work perfectly, and we'll have to figure out why.
Oh, and one more thing. Here's the formula for calculating FVA:
FVA = CF * (((1 + i)^n-1)/i)
With a formula like that, what can possibly go wrong? Let's put the code together and see what we come up with.
Example Code
Make a new C# class library project targeting ".Net Framework." Name it NCrunch.UnitTestDebugging. Add a folder named Tests. We're going to work in that folder only. In a real-world solution, you might have separate projects for tests and source.
You'll also need to install the xUnit NuGet package for the unit tests.
Install-Package xunit
In this tutorial, we're going to keep the code simple.
Add a new class file named "AnnuityTests.cs" to the Tests folder.
using Xunit;
namespace NCrunch.UnitTestDebugging.Tests
{
public class AnnuiteTests
{
[Theory] // a theory in xUnit is a test that takes parameters
[InlineData(1, 100, 5, 105)]
public void ComputesExpectedFutureValue(int n, decimal CF, decimal i, decimal FVA)
{
var actual = Annuity.ComputeFutureValue(n, CF, i);
Assert.Equal(FVA, actual);
}
}
// Keep the Annuity class in the same file for now.
// I normally start developing a new class in the same file this way.
// Eventually, I'll move it into it's own file.
public class Annuity
{
public static decimal ComputeFutureValue(int n, decimal CF, decimal i)
{
return 105;
}
}
}
For now, we'll just return 105 and get the test running. The test framework isn't so important; we can debug unit tests just the same with MS Test, NUnit, or even some custom framework. Let's run this test and see what happens.
Run the Example in Visual Studio
Two words: keyboard shortcuts! The shortcut for running a test is Ctrl R, T for "run test"—get it?!
If you want to run one test, put your cursor inside the test. If you put your cursor in the test class outside of method, the test runner will run all tests in the class. You can do the same for the namespace. It's context aware! That's part of Visual Studio, so it works regardless of which test framework you choose.
After you've run the example, you should see the following test results:
The test explorer should open when you execute your tests. We expect the tests to either pass or fail. In either case, the icon should be a green checkmark or a red x. But instead, I'm getting this blue warning icon. Something isn't quite right. Let's walk through resolving this issue before we debug the tests themselves.
Resolve Test Runner Issues
It's not uncommon to come across issues like this when you first start unit testing. In this case, we can look into the "Output" pane to see what's been logged. I see the following warning from the "Tests" output:
No test matches the given testcase filter \
`FullyQualifiedName=NCrunch.UnitTestDebugging.Tests.AnnuiteTests.ComputesExpectedFutureValue` in...
Even though we have a test case, it's not being recognized by the test runner. Let's try adding a different kind of test to see if it gets picked up. This time we'll add a "Fact" type of test. These tests take no input. Sometimes "Theory" test types can be tricky because of the specifics around how the data is passed to the tests.
Add the following test method to see if it's "InputData"-related or something more:
[Fact]
public void AssertsTrue() => Assert.True(true);
Once you add this method, it should show up in the "Test Explorer." You can click "Run All" at the top of that pane to see what happens.
We still have zero tests being recognized. We need to add another NuGet package—one that allows our tests to run in Visual Studio.
Add xUnit Runner for Visual Studio
It's a good thing we went through this exercise before we made a bunch of tests! Also, it's an excellent opportunity to learn something important about xUnit. It's not like I did this on purpose or anything (wink, wink).
xUnit comes in two parts: the core and the test runner. You have options for which runner to add. There's a console-based runner and a runner for Visual Studio. Let's add the Visual Studio runner now.
Install-Package xunit.runner.visualstudio
This NuGet package allows Visual Studio to discover xUnit tests in your solution. With the test runner installed, let's rerun the test. You should see something like the following in your test explorer and output panels:
A successful test run looks like the screenshot above. All systems go! Now let's make a test that will fail.
Make a Failing Test
Usually, I'll write C# code to an interface. And I might use a fake or a mock as a stand-in while testing. I often use the "implement interface" feature in Visual Studio to stub out the methods. That default implementation throws an exception. Of course, uncaught exceptions will fail a unit test, so this is a common cause for failed tests. We can use this as a simple example to foray into debugging a unit test.
Here's a concrete example:
// Create this interface
public interface ICalculable
{
string Name { get; }
decimal Value { get; }
}
// Remove unneeded tests
public class AnnuiteTests
{
[Theory]
[InlineData(1, 100, 5, 105)]
public void ComputesExpectedFutureValue(int n, decimal CF, decimal i, decimal FVA)
{
// Update Test to use Annuity instance
ICalculable annuity = new Annuity(n, CF, i);
Assert.Equal(FVA, annuity.Value);
}
// Add test for Name
[Fact]
public void NameIsAnnuity()
{
ICalculable annuity = new Annuity(1, 1, 1);
Assert.Equal("Annuity", annuity.Name);
}
}
// Implement interface and remove static method
public class Annuity : ICalculable
{
private readonly int n;
private readonly decimal cf;
private readonly decimal i;
public Annuity(int n, decimal cf, decimal i)
{
this.n = n;
this.cf = cf;
this.i = i;
}
public string Name => throw new System.NotImplementedException();
public decimal Value => 105;
}
Once you run these tests, you should get a failed unit test. "Annuity.Name" throws an exception, which causes the test to fail. It should return "Annuity" instead.
Even though it's dead simple to figure out what's going on here, we'll debug the failing unit test.
Debug the Unit Test
We've encountered a failed test run. Oh, no! What do we do? Let's check the failure first before we debug the unit test....
Clicking on the failed test opens the details in the lower section of the test explorer. You might have to adjust the dividers to get the whole picture, but you can see in the screenshot that the failure is on line 26. Click that link to go directly to the unit test code.
Press F9 to drop a breakpoint, and hit Ctrl+R, Ctrl+T (hold control throughout and press R, T) to debug the test. You should now see something like the following in Visual Studio:
A lot is going on here, but you now have all the tools you need to debug your unit tests and your code. Let's go over what you see in this image:
- What process are you debugging? The test runner is the executing process. Sometimes, you can get in trouble by expecting the code to always execute in the context of a web app, for example. Nowadays, unless you're unit testing legacy code, you can set the context and pass it to your subject under test.
- In red circle two, you have the current thread. When you're testing with concurrency, you can switch threads here. That can come in handy!
- Of course, the diagnostic tools can help when you're debugging a slow test. If you have a slow test, you probably have a slow subject under test. With a really large test suite, it can take a while to get the first ones up and running, but the rest should go smoothly.
- The pane in the lower-left corner shows you all the variables in context. Note the three contexts you can switch between in this pane: Autos, Locals, and Watch 1.
- Finally, at the lower right, we have the infamous call stack, where you can see the current execution path through the code. This is also where you can check breakpoints and settings, and run arbitrary expressions in the Immediate window. And when in doubt, check the output.
If you have some failing code, one of the best things you can do is cover it with a unit test first. This is one of the most effective ways to drive your buggy code! You can run and debug unit tests very quickly using the shortcut methods in this post.
Your debugging skills should be ready to take over by now, so let's wrap this one up.
Put a Bow on It
In this post, you've seen how to create, run, and debug a unit test. It turns out it's not much different from debugging any code! Unit tests, after all, are just another type of code. They have a specific purpose: to test your code and keep it bug-free. Other than writing tests for the tests, if something's wrong in your unit test, your only recourse is to fire up the debugger. And now you know how to!
This post was written by Phil Vuollet. Phil uses software to automate processes to improve efficiency and repeatability. He writes about topics relevant to technology and business, occasionally gives talks on the same topics, and is a family man who enjoys playing soccer and board games with his children.