Many developers will have strong opinions on unit testing. If you are considering using unit tests in your code, then start by understanding why unit tests are useful and sometimes necessary.
Breaking down your code’s functionality into smaller, testable units of behavior allows you to create and run unit tests. Unit tests will increase the likelihood that your code will continue to work as expected, even though you have made changes to the source code. In this chapter, we will have a look at
Creating and running unit tests
Using live unit tests
Using IntelliTest to generate unit tests
How to measure Code Coverage in Visual Studio
Unit tests allow you to maintain the health of your code and find errors quickly, before shipping your application to your customers. To introduce you to unit testing, we will start off with a very basic example of creating a unit test.
Creating and Running Unit Tests
Assume that you have a method that calculates the temperature in Fahrenheit for a given temperature in Celsius. The code that we want to create a unit test for will look as in Listing 4-1.
public static class ConversionHelpers
{
private const double F_MULTIPLIER = 1.8;
private const int F_ADDITION = 32;
public static double ToFahrenheit(double celsius)
{
return celsius * F_MULTIPLIER + F_ADDITION;
}
}
Listing 4-1
Convert Celsius to Fahrenheit
We have constant values for the multiplier and addition to the conversion formula. This means that we can easily write a test to check that the conversion is an expected result.
Start off by adding a new Unit Test project to your solution. You will see (Figure 4-1) that you have the option to add a Unit Test project template for the test framework you prefer to use.
Once you have added your Unit Test project to your solution, it will appear in the solution with a different icon indicating that it is a Unit Test project (Figure 4-2).
To effectively test the class that contains the method that converts Celsius to Fahrenheit, we need to reference that class in our Unit Test project. Right-click the Unit Test project and add a reference to the project containing the class we need to test (Figure 4-3).
When the reference has been added to your test project, create the test as seen in Listing 4-2.
[TestClass]
public class ConversionHelperTests
{
[TestMethod]
public void Test_Fahrenheit_Calc()
{
// arrange - setup
var celsius = -7.0;
var expectedFahrenheit = 19.4;
// act - test
var result = ConversionHelpers.ToFahrenheit(celsius);
// assert - check
Assert.AreEqual(expectedFahrenheit, result);
}
}
Listing 4-2
Unit Test for Fahrenheit
When you look at the code in Listing 4-2, you will notice that we do three things in each test. These are
Arrange – Where we set up the test
Act – Where we test the code to get a result
Assert – Where we check the actual result against the expected result
From the Test menu, select Test Explorer, or hold down Ctrl+E, T. In Test Explorer, click the green play button to run the test and see the test results displayed (Figure 4-4).
From the results displayed in the Test Explorer, you can easily see which tests failed and which have passed. From our rather simple test in Listing 4-2, you can see that the test passed easily and that the result we expected was indeed the actual result of the test. Note that our test compares two type double values for exact equality. The Assert.AreEqual method has an overload that accepts an error tolerance parameter.
To see what happens when a test fails, modify the Integer value for the constant F_ADDITION variable as seen in Listing 4-3.
private const double F_MULTIPLIER = 1.8;
private const int F_ADDITION = 33;
public static double ToFahrenheit(double celsius)
{
return celsius * F_MULTIPLIER + F_ADDITION;
}
Listing 4-3
Modify the Fahrenheit Constant
Running the tests again after the change will result in a failed test as seen in Figure 4-5. The change we made was a small change, but it’s easy to miss this if we work in a team and on a big code base.
What the unit test does is to keep an eye on the quality of the code as it changes throughout development. This is especially important when working in a team. It will allow other developers to see if any code changes they have made have broken some intended functionality in the code.
In Visual Studio 2022, you can also run the tests by right-clicking the test project and selecting Run Tests from the context menu.
The Test Explorer offers a lot of functionality, and you can see this from looking at the labels on the image in Figure 4-6.
From the Test Explorer, you can
Run all tests or just the last test
Only run failed tests (great if you have many tests in your project)
Filter the test results
Group tests
Start Live Unit Testing (more on this later)
Create and run a test playlist
Modify test settings
Let’s have a look at creating a test playlist.
Create and Run a Test Playlist
If your project contains many tests, and you want to run those tests as a group, you can create a playlist. To create a playlist, select the tests that you want to group from the Test Explorer, and right-click them. From the context menu that pops up, select Add to Playlist ➤ New Playlist as seen in Figure 4-7.
This will open a new Test Explorer window where you can run the tests and save the tests you selected under a new playlist name. This will create a .playlist file for you.
I created a new playlist called Temperature_Tests.playlist from the Celsius and Fahrenheit temperature conversion tests. The playlist file it creates is simply an XML file that in my example looks as in Listing 4-4.
To open and run a playlist again, click the Create or run test playlist button and select the playlist file you want to run.
Testing Timeouts
The speed of your code is also very important. If you are using the MSTest framework, you can set a timeout attribute to set a timeout after which a test should fail. This is convenient because as you write code for a specific method, you can immediately identify if the code you are adding to a method is causing a potential bottleneck. Consider the Test_Fahrenheit_Calc test we created earlier.
[TestMethod]
[Timeout(2000)]
public void Test_Fahrenheit_Calc()
{
// arrange - setup
var celsius = -7.0;
var expectedFahrenheit = 19.4;
// act - test
var result = ConversionHelpers.ToFahrenheit(celsius);
// assert - check
Assert.AreEqual(expectedFahrenheit, result);
}
Listing 4-5
Adding a Timeout Attribute
As seen in Listing 4-5, I have added a timeout of 2000 milliseconds. If you run your tests now, it will pass because the calculation it performs is all it does. To see the timeout attribute in action, swing back to the ToFahrenheit method in the ConversionHelpers class and modify it by sleeping the thread for 2.5 seconds as seen in Listing 4-6.
public static double ToFahrenheit(double celsius)
{
Thread.Sleep(2500);
return celsius * F_MULTIPLIER + F_ADDITION;
}
Listing 4-6
Sleeping the Thread
Run your tests again and see that, this time, your test has failed because it has exceeded the specified timeout value set by the Timeout attribute (Figure 4-8).
Identifying critical methods in your code and setting a specific timeout on that method will allow developers to catch issues early on when tests start exceeding the timeout set. You can then go back and immediately refactor the code that was recently changed to improve the execution time.
Using Live Unit Tests
First introduced in Visual Studio 2017, Live Unit Testing runs your unit tests automatically as you make changes to your code. You can then see the results of your unit tests in real time.
Live Unit Testing is only available in Visual Studio Enterprise edition for C# and Visual Basic projects targeting the .NET Framework or .NET Core. For a full comparison between the editions of Visual Studio, refer to the following link: https://visualstudio.microsoft.com/vs/compare/.
The benefits of Live Unit Testing are as follows:
You will immediately see failing tests, allowing you to easily identify breaking code changes.
It indicates Code Coverage, allowing you to see what code is not covered by any unit tests.
Live Unit Testing persists the data of the status of the tests it ran. It then uses the persisted data to dynamically run your tests as your code changes. Live Unit Testing supports the following test frameworks:
xUnit.net – Minimum version xunit 1.9.2
NUnit – Minimum version NUnit version 3.5.0
MSTest – Minimum version MSTest.TestFramework 1.0.5-preview
Before you can start using Live Unit Testing, you need to configure it by going to Tools ➤ Options and selecting Live Unit Testing in the left pane (Figure 4-9).
Once you have configured the Live Unit Testing options, you can enable it from Test ➤ Live Unit Testing ➤ Start. To see the Live Unit Testing window, click the Live Unit Testing button as seen in Figure 4-6.
The Live Unit Testing window is displayed as seen in Figure 4-10.
Make some breaking changes to your code and save the file. You will see that the Live Unit Testing window is updated to display the failing tests as seen in Figure 4-11.
Live Unit Testing gives you a good insight into the stability of the code you write, as you write the code. Let’s go a little further. Add the class in Listing 4-7 to your project under test.
public class Container : ICloneable
{
public string ContainerNumber { get; set; }
public string ShipNumber { get; set; }
public double Weight { get; set; }
public object Clone() => throw new NotImplementedException();
}
Listing 4-7
Container Class Implementing ICloneable
Don’t add any implementation to the Clone method. Swing back to the test project and add a Unit Test for the Containerclass as in Listing 4-8.
[TestMethod]
public void Test_Container()
{
var containerA = new Container();
var containerB = containerA.Clone();
var result = (containerA == containerB);
Assert.IsFalse(result);
}
Listing 4-8
Unit Test for the Container Class
Start Live Unit Testing, and you will notice that your test fails as seen in Figure 4-12.
Have a look at the Containerclass, and you will notice that Live Unit Testing has also updated the code file with the faulting method (Figure 4-13).
As soon as you add implementation to the Clonemethod, your Live Unit Test results are updated as seen in Figure 4-14.
With Live Unit Testing, areas of code indicated by a dash are not covered by any tests. A green tick indicates that the code is covered by a passing test. A red X indicates that the code is covered by a failing test.
Using IntelliTest to Generate Unit Tests
IntelliTest helps developers generate and get started using Unit Tests. This saves a lot of time writing tests and increases code quality.
IntelliTest is only available in Visual Studio Enterprise edition.
The default behavior of IntelliTest is to go through the code and try to create a test that gives you maximum Code Coverage. To illustrate how IntelliTest works, I will create a simple class that calculates shipping costs as seen in Listing 4-9.
public class Calculate
{
public enum ShippingType { Overnight = 0, Priority = 1, Standard = 2 }
private const double VOLUME_FACTOR = 0.75;
public double ShippingCost(double length, double width, double height, ShippingType type)
{
var volume = length * width * height;
var cost = volume * VOLUME_FACTOR;
switch (type)
{
case ShippingType.Overnight:
cost = cost * 2.25;
break;
case ShippingType.Priority:
cost = cost * 1.75;
break;
case ShippingType.Standard:
cost = cost * 1.05;
break;
default:
break;
}
return cost;
}
}
Listing 4-9
Calculate ShippingCost Method
To run IntelliTest against the ShippingCostmethod, right-click the method, and click IntelliTest ➤ Run IntelliTest from the context menu. The results will be displayed in the IntelliTest window that pops up as seen in Figure 4-15. You can also see the details of the generated unit test in the Details pane.
IntelliTest has taken each parameter going to the method and generated a parameter value for it. In this example, all the tests succeeded, but there is still a problem. This is evident from the result value which is always zero.
We can never allow a parcel to be shipped with zero shipping cost, no matter how small it is. What becomes clear here is that we need to implement minimum dimensions. We, therefore, need to modify the Calculateclass as in Listing 4-10.
public class Calculate
{
public enum ShippingType { Overnight = 0, Priority = 1, Standard = 2 }
private const double VOLUME_FACTOR = 0.75;
private const double MIN_WIDTH = 1.5;
private const double MIN_LENGTH = 2.5;
private const double MIN_HEIGHT = 0.5;
public double ShippingCost(double length, double width, double height, ShippingType type)
{
if (length <= 0.0) length = MIN_LENGTH;
if (width <= 0.0) width = MIN_WIDTH;
if (height <= 0.0) height = MIN_HEIGHT;
var volume = length * width * height;
var cost = volume * VOLUME_FACTOR;
switch (type)
{
case ShippingType.Overnight:
cost = cost * 2.25;
break;
case ShippingType.Priority:
cost = cost * 1.75;
break;
case ShippingType.Standard:
cost = cost * 1.05;
break;
default:
break;
}
return cost;
}
}
Listing 4-10
Modified Calculate Class
Running IntelliTest again yields a completely different set of results as seen in Figure 4-16.
This time, you can see that no matter what the value of the parcel dimensions is, we will always have a result returned for the shipping costs. To create the unit tests generated by IntelliTest, click the Save button in the IntelliTest window.
This will create a new Unit Test project for you in your solution as seen in Figure 4-17.
You can now run the generated Unit Tests as you normally would with Test Explorer. As you continue coding and adding more logic to the Calculate class, you can regenerate the Unit Tests by running IntelliTest again. IntelliTest will then crawl through your code again and generate new Unit Tests for you to match the logic of your code at that time.
The underlying engine that IntelliTest uses to crawl through your code and generate the Unit Tests is Pex. Pex is a Microsoft Research project that was never productized or supported until IntelliTest started using it.
For a moment, I want you to think back to the code in Listing 4-10. Remember how we modified the code to include constant values to cater for IntelliTest setting the default parameter values to zero? Imagine for a minute that we will never receive a zero as a parameter and that this check is built into the calling code. We can tell IntelliTest to assume values for these parameters.
Have a look at Figure 4-17, and locate the CalculateTest partial class generated for you by IntelliTest. The code generated for you is in Listing 4-11.
We are now going to tell the Pex engine that we want to assume certain values for the parameters. We do this by using PexAssume.
PexAssume is a static helper class containing a set of methods to express preconditions in parameterized Unit Tests.
Modify the code in the CalculateTest partial class’ ShippingCost method by adding PexAssume.IsTrue as a precondition for each parameter as illustrated in Listing 4-12.
[PexMethod]
public double ShippingCost(
[PexAssumeUnderTest]Calculate target,
double length,
double width,
double height,
Calculate.ShippingType type
)
{
PexAssume.IsTrue(length > 0);
PexAssume.IsTrue(width > 0);
PexAssume.IsTrue(height > 0);
double result = target.ShippingCost(length, width, height, type);
By doing this, I can now modify my Calculate class to remove the constant values ensuring that the length, width, and height parameters are greater than zero. The Calculateclass will now look as in Listing 4-13.
public class Calculate
{
public enum ShippingType { Overnight = 0, Priority = 1, Standard = 2 }
private const double VOLUME_FACTOR = 0.75;
public double ShippingCost(double length, double width, double height, ShippingType type)
{
var volume = length * width * height;
var cost = volume * VOLUME_FACTOR;
switch (type)
{
case ShippingType.Overnight:
cost = cost * 2.25;
break;
case ShippingType.Priority:
cost = cost * 1.75;
break;
case ShippingType.Standard:
cost = cost * 1.05;
break;
default:
break;
}
return cost;
}
}
Listing 4-13
Modified Calculate Class
Run IntelliTest again, and see that the parameter values passed through are never zero (Figure 4-18).
You can modify the CalculateTest partial class by adding assertions to the ShippingCost method. When you expand CalculateTest in the Solution Explorer (Figure 4-19), you will see several ShippingCost test methods listed.
These correspond to the IntelliTest results as seen in Figure 4-18. Do not modify these code files, as your changes will be lost when IntelliTest is run again and it regenerates those tests.
Focus IntelliTest Code Exploration
Sometimes, IntelliTest needs a bit of help focusing code exploration. This can happen if you have an Interface as a parameter to a method and more than one class implements that Interface. Consider the code in Listing 4-14.
public class ShipFreight
{
public void CalculateFreightCosts(IShippable box)
{
}
}
class Crate : IShippable
{
public bool CustomsCleared { get; }
}
class Package : IShippable
{
public bool CustomsCleared { get; }
}
public interface IShippable
{
bool CustomsCleared { get; }
}
Listing 4-14
Focusing Code Exploration
If you ran IntelliTest on the CalculateFreightCostsmethod, then you will receive warnings as can be seen in Figure 4-20.
You can tell IntelliTest which class to use to test the interface. Assume that I want to use the Package class to test the Interface. Now, just select the second warning and click the Fix button on the menu as seen in Figure 4-21.
IntelliTest now updates the PexAssemblyInfo.cs file by adding [assembly: PexUseType(typeof(Package))] to the end of the file to tell IntelliTest which class to use. Running IntelliTest again results in no more warnings being displayed.
How to Measure Code Coverage in Visual Studio
Code Coverage indicates what portion of your code is covered by Unit Tests. To guard against bugs, it becomes obvious that the more code is covered by Unit Tests, the better tested it is.
IntelliTest is only available in Visual Studio Enterprise edition.
The Code Coverage feature in Visual Studio will give you a good idea of your current Code Coverage percentage. To run the Code Coverage analysis, open up Test Explorer, and click the drop-down next to the play button (Figure 4-22).
Click Analyze Code Coverage for All Tests in the menu.
You can also go to the Test menu, click Windows, and click Test Explorer.
The Code Coverage Results are then displayed in a new window (Figure 4-23). You can access this window from the Test menu and then select Windows ➤ Code Coverage Results or hold down Ctrl+E, C on the keyboard.
In the Code Coverage Results window, you can Export the results, Import Results, Merge Results, Show Code Coverage Color (Figure 4-24), or Remove the results.
This will toggle colors in your code editor to highlight areas touched, partially touched, and not touched at all by tests. The colors used to highlight the code can also be changed. To do this, head on over to Tools ➤ Options ➤ Environment ➤ Fonts and Colors (Figure 4-25).
This should give you a good understanding of how much code is covered by unit tests. Developers should typically aim for at least 80% Code Coverage. If the Code Coverage is low, then modify your code to include more tests. Once you are done modifying your code, run the Code Coverage tool again as the results are not automatically updated as you modify your code.
Code Coverage is typically measured in blocks. A block of code is a section of code that has exactly one entry point and one exit point. If you prefer to see the Code Coverage in terms of lines covered, you can change the results by choosing Add/Remove Columns in the results table header (Figure 4-26).
Code Coverage is a great tool to allow you to check if your code is sufficiently covered by unit tests. If you aim for 80% Code Coverage, you should be able to produce well-tested code. The 80% Code Coverage is not always attainable. This is especially true if the code base you’re working on has a lot of generated code. In instances such as these, a lower percentage of code cover is acceptable.
Summary
Unit testing in Visual Studio helps developers maintain the health of their code and find errors quickly, before shipping their applications to their customers. While some features are not available in all editions of Visual Studio, the free Visual Studio Community does offer developers some unit test functionality. This is enough to get them by. In the next chapter, we will be looking at a feature that all developers should be very familiar with. Source control management is essential to any project. We will explore this and some new features of Visual Studio 2022 such as multi-repo support, comparing branches, Checkout Commit, and line staging but to name a few. Let’s look at source control next.