© Dirk Strauss 2020
D. StraussGetting Started with Visual Studio 2019https://doi.org/10.1007/978-1-4842-5449-3_4

4. Unit Testing

Dirk Strauss1 
(1)
Uitenhage, South Africa
 

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig1_HTML.jpg
Figure 4-1

Add a new Unit Test project

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig2_HTML.jpg
Figure 4-2

Unit Test project added to the solution

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).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig3_HTML.jpg
Figure 4-3

Reference class to test

To effectively test the class that contains the method that converts Celcius 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 a given 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 Windows and then 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).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig4_HTML.jpg
Figure 4-4

Running your Unit Test

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig5_HTML.jpg
Figure 4-5

Failed test results for Fahrenheit calculation

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 has broken some intended functionality in the code.

In Visual Studio 2019, 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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig6_HTML.jpg
Figure 4-6

Test Explorer Menu

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig7_HTML.jpg
Figure 4-7

Create a Playlist

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.
<Playlist Version="1.0">
<Add Test="VisualStudioTests.ConversionHelperTests.Test_Fahrenheit_Calc" />
<Add Test="VisualStudioTests.ConversionHelperTests.Test_Celsius_Calc" />
</Playlist>
Listing 4-4

Temperature_Tests.playlist file contents

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 really 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).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig8_HTML.jpg
Figure 4-8

Test timeout exceeded

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 in order 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
  • You will immediately see failing tests allowing you to easily identify breaking code changes

  • 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 Live Unit Testing from Tools, Options and selecting Live Unit Testing in the left pane (Figure 4-9).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig9_HTML.jpg
Figure 4-9

Configure Live Unit Testing

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig10_HTML.jpg
Figure 4-10

Live Unit Testing window

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig11_HTML.jpg
Figure 4-11

Live Unit Testing results updated

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 following class to your project under test (Listing 4-7):
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 Container class 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 Container Class

Start Live Unit Testing, and you will notice that your test fails as seen in Figure 4-12.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig12_HTML.jpg
Figure 4-12

Live Unit Test Results Failed

Have a look at the Container class, and you will notice that Live Unit Testing has also updated the code file with the faulting method (Figure 4-13).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig13_HTML.jpg
Figure 4-13

Container Class Live Unit Test results

As soon as you add implementation to the Clone method , your Live Unit Test results are updated as seen in Figure 4-14.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig14_HTML.jpg
Figure 4-14

Implementing the Clone method

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 ShippingCost method, 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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig15_HTML.jpg
Figure 4-15

IntelliTest Results

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 a 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 Calculate class 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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig16_HTML.jpg
Figure 4-16

IntelliTest Results on modified class

This time you can see that no matter what the value of the parcel dimensions are, 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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig17_HTML.jpg
Figure 4-17

Generated Unit Tests

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.
[TestClass]
[PexClass(typeof(Calculate))]
[PexAllowedExceptionFromTypeUnderTest(typeof(ArgumentException), AcceptExceptionSubtypes = true)]
[PexAllowedExceptionFromTypeUnderTest(typeof(InvalidOperationException))]
public partial class CalculateTest
{
    [PexMethod]
    public double ShippingCost(
        [PexAssumeUnderTest]Calculate target,
        double length,
        double width,
        double height,
        Calculate.ShippingType type
    )
    {
double result = target.ShippingCost(length, width, height, type);
        return result;
        // TODO: add assertions to method CalculateTest.ShippingCost(Calculate, Double, Double, Double, ShippingType)
    }
}
Listing 4-11

Generated CalculateTest Partial Class

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);
    return result;
    // TODO: add assertions to method CalculateTest.ShippingCost(Calculate, Double, Double, Double, ShippingType)
}
Listing 4-12

Modified CalculateTest Partial Class

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 Calculate class 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).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig18_HTML.jpg
Figure 4-18

IntelliTest Results with PexAssume

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig19_HTML.jpg
Figure 4-19

ShippingCost Generated Tests

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 CalculateFreightCosts method, then you will receive the following warnings as can be seen in Figure 4-20.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig20_HTML.jpg
Figure 4-20

Focus Code Exploration

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig21_HTML.jpg
Figure 4-21

Tell IntelliTest which class to use

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).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig22_HTML.jpg
Figure 4-22

Analyze Code Coverage

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.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig23_HTML.jpg
Figure 4-23

Code Coverage Results

In the Code Coverage Results window, you can Export the results, Import Results, Merge Results, Show Code Coverage Color, or Remove the results.
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig24_HTML.jpg
Figure 4-24

Toggle Code Coverage Coloring

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).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig25_HTML.jpg
Figure 4-25

Change Fonts and Colors

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).
../images/487681_1_En_4_Chapter/487681_1_En_4_Fig26_HTML.jpg
Figure 4-26

Code Coverage Expressed in Lines

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.35.60