Chapter 4. Unit testing with xUnit

This chapter covers

  • Executing unit tests with the .NET CLI
  • Writing unit tests with xUnit
  • The difference between facts and theories
  • Logging test output

Testing is an essential part of writing great libraries and applications, and the first line of defense for testing is the unit test. In this chapter you’ll learn how to write unit tests in .NET Core, execute the tests, and add and collect logging data.

4.1. Why write unit tests?

Unit tests are written against the smallest units in a software library, such as classes. A class should have responsibility over a single piece of functionality, and that responsibility should be entirely encapsulated by the class. This is the single responsibility principle, and it’s one of the SOLID software design principles.

SOLID software design

The SOLID design principles are part of the core principles of the agile software development methodology. They’re also just good practice. I recommend reading Clean Code: A Handbook of Agile Software Craftsmanship by Robert C. Martin (Prentice Hall, 2008) to learn more.

When developing a software application, it’s common to want to build all the pieces and test the software’s functionality as a whole. The problem with this approach is that it’s too easy to miss corner cases or unanticipated scenarios. Unit testing gets the developer to think about each unit individually and to verify those units independently.

Unit tests also help build confidence in an application’s functionality. When the unit tests achieve a high percentage of code coverage, you know that most of the code has been exercised (although you should avoid relying too much on this metric). The tests also enforce a contract for each unit, so any changes to the source code must either comply with the existing contract or explicitly change the contract.

Refactoring

Modifying the code without changing its external behavior is called refactoring. Unit tests are an essential part of the refactoring process.

I believe that unit testing is an essential tool for building software. You’ll use unit testing throughout this book, so it’s important to introduce it early. The .NET CLI seamlessly integrates with unit-testing frameworks and makes them a primary function. Unit testing is easier to learn if you start with some sample code first and build unit tests for that code.

4.2. Business-day calculator example

I once worked on a manufacturer/supplier collaboration application. The premise was simple: the manufacturer wanted to communicate with all of its suppliers to make sure it had supplies at the right time and in the right quantities so as not to interrupt the assembly line. In this case, it was imperative that the right dates were calculated. Most suppliers gave estimates in the form of a number of business days, so I was tasked with coming up with a way to display the estimated date of a shipment’s delivery based on when the order was placed.

To my inexperienced ears, the problem sounded simple. But this was back before I knew what unit testing was for. The component I wrote didn’t correctly calculate dates where suppliers had different holiday schedules than the manufacturer, and I missed this bug because I didn’t have unit tests. Luckily, the manufacturer, our customer, noticed the issue before it caused an assembly-line mishap.

In this chapter, you’ll write a business-day calculator and the accompanying unit tests. The concept may seem trivial, but calculating the date incorrectly can cost your customer or employer money and can cost you a contract. You’ll make the library able to calculate a target date based on a start date and the number of business days. You’ll focus on US holidays for now, but you’ll leave the library open to work with other nations.

You’ll create two projects in a folder called BusinessDays. The folder structure and project files should look like this:

  • BusinessDays

    • BizDayCalc

      • BizDayCalc.csproj
    • BizDayCalcTests

      • BizDayCalcTests.csproj

First, create the three folders. At the command prompt, go to the BizDayCalc folder and execute dotnet new classlib. You haven’t used this template before—it’s for creating class libraries. Class libraries don’t have an entry point and can’t run on their own, but they can be referenced by other projects.

Rename the Class1.cs file to Calculator.cs, and insert the following code.

Listing 4.1. Contents of Calculator.cs
using System;                                           1
using System.Collections.Generic;                       2

namespace BizDayCalc
{
  public class Calculator
  {
    private List<IRule> rules = new List<IRule>();

    public void AddRule(IRule rule)
    {
      rules.Add(rule);
    }

    public bool IsBusinessDay(DateTime date)
    {
      foreach (var rule in rules)
        if (!rule.CheckIsBusinessDay(date))             3
          return false;

      return true;
    }
  }
}

  • 1 Needed for DateTime
  • 2 Needed for List<>
  • 3 If a rule reports false, it’s not a business day.

The Calculator class allows you to add a set of rules, and then it tests those rules against a given date. If any rule returns false, it’s not a business day.

Each rule is singularly responsible for its logic. If you attempt to put all the logic together into one long string of if statements, you’ll end up with complex code that’s hard to maintain. Dividing the logic into rules also gives applications using the BizDayCalc library the ability to customize. For instance, you could have a rule for President’s Day, which some companies may observe as a holiday and others may not. The application offers the user the freedom to choose.

A rule implements the IRule interface. IRule only has one method: CheckIsBusinessDay. The goal is to make implementing a rule as simple as possible. The Calculator class can determine how many business days fall within a given date range or work out an estimated date based on the number of business days, using only the CheckIsBusinessDay method.

You’re going to add the code for the IRule interface. Create a new file called IRule.cs and insert the following code.

Listing 4.2. Contents of IRule.cs
using System;

namespace BizDayCalc
{
  public interface IRule
  {
    bool CheckIsBusinessDay(DateTime date);
  }
}

So far you haven’t defined any actual business logic. For many companies, weekends aren’t considered business days, so define a rule that checks the day of the week. Create a new file called WeekendRule.cs and add the following code.

Listing 4.3. Contents of WeekendRule.cs
using System;

namespace BizDayCalc
{
  public class WeekendRule : IRule
  {
    public bool CheckIsBusinessDay(DateTime date)
    {
      return
        date.DayOfWeek != DayOfWeek.Saturday &&
        date.DayOfWeek != DayOfWeek.Sunday;
    }
  }
}

You’ll build more of this library throughout this chapter. You have enough now to start creating unit tests.

4.3. xUnit—a .NET Core unit-testing framework

xUnit is a unit-testing framework. Each unit-testing framework is different, but they can all interface with the .NET CLI to execute tests.

The .NET CLI command to run tests is dotnet test, and you’ll be using it throughout this chapter. Other unit-testing frameworks will also work with dotnet test, but each framework has different ways of writing unit tests. xUnit worked with .NET Core very early on, so it’s a favorite of early adopters. Plus, there’s a xUnit template for dotnet new built into the CLI.

xUnit takes advantage of features available in .NET that older .NET unit-testing frameworks don’t. This makes for a more powerful testing framework that’s easier to code, and it incorporates recent advances in unit testing.

xUnit philosophy

xUnit.net on GitHub has a great explanation of why xUnit was built in an article titled “Why did we build xUnit 1.0?” (http://mng.bz/XrLK).

4.4. Setting up the xUnit test project

Go to the parent folder, BusinessDays, and create a new subfolder called BizDayCalcTests. Inside the subfolder, run dotnet new xunit. Modify the BizDayCalcTests.csproj file as shown in the following listing.

Listing 4.4. Modifying BizDayCalcTests.csproj to reference the BizDayCalc project
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk"
                      Version="15.3.0" />
    <PackageReference Include="xunit" Version="2.2.0" />
    <PackageReference Include="xunit.runner.visualstudio"
                      Version="2.2.0" />                                1
    <ProjectReference                                                   2
                      Include="../BizDayCalc/BizDayCalc.csproj" />
  </ItemGroup>

</Project>

  • 1 xUnit test runner, so you can use dotnet test
  • 2 References the BizDayCalc class library

You’ll learn how to use the xUnit CLI runner in the next section.

4.5. Evaluating truth with xUnit facts

The first test you’ll write is for the WeekendRule.cs file. Rename the UnitTest1.cs file to WeekendRuleTest.cs, and modify the code to look like the following.

Listing 4.5. WeekendRuleTest.cs using Fact
using System;
using BizDayCalc;
using Xunit;

namespace BizDayCalcTests
{
  public class WeekendRuleTest
  {
    [Fact]                                         1
    public void TestCheckIsBusinessDay()
    {
      var rule = new WeekendRule();
      Assert.True(rule.CheckIsBusinessDay(new DateTime(2016, 6, 27)));
      Assert.False(rule.CheckIsBusinessDay(new DateTime(2016, 6, 26)));
    }
  }
}

  • 1 Fact is a type of xUnit test.
For those not familiar with C#

[Fact] is an attribute, and it indicates that the method is a unit test. Attributes can be applied on classes, members of classes, or even whole assemblies, and they provide information about an item. The full type name of [Fact] is Xunit.FactAttribute, and it’s part of the xUnit library. By convention, C# will assume the Attribute suffix.

Execute dotnet test to run the test. You should see output similar to the following.

Starting test execution, please wait...
[xUnit.net 00:00:00.7711235]   Discovering: BizDayCalcTests
[xUnit.net 00:00:00.9131241]   Discovered:  BizDayCalcTests
[xUnit.net 00:00:00.9663611]   Starting:    BizDayCalcTests
[xUnit.net 00:00:01.1293488]   Finished:    BizDayCalcTests

Total tests: 1. Passed: 1. Failed: 0. Skipped: 0.
Test Run Successful.

Now that you’ve successfully run a unit test, let’s explore this test a bit. The first thing to notice is [Fact]. It indicates that the test is always true. Also note that the Assert class, which is a common pattern in unit-testing frameworks, has methods like True and False instead of IsTrue and IsFalse. xUnit generally leaves out verbs in the method names.

How does dotnet test work?

There’s no built-in support for xUnit in .NET Core. Yet the dotnet test command is so simple, it feels like xUnit is integrated somehow.

The magic is down to the xunit.runner.visualstudio package. If you were to peek at the code in the package, you’d find assemblies with entry points. The dotnet test command is essentially running another application and passing it parameters. This makes it easy for testing frameworks to work with .NET Core.

Also note that the .NET Core test runner operates in both a console mode and a design-time mode. The design-time mode is used by IDEs such as Visual Studio. In the IDE, the test runner operates slightly differently.

It’s also important to note what isn’t in this code. There’s no attribute on the WeekendRuleTest class that indicates it’s a test class. xUnit avoids the use of such devices to make the code cleaner. The presence of test methods is enough to tell xUnit that it’s a test class.

4.6. Running tests from development environments

Most development environments integrate testing into the editor. For example, OmniSharp adds links to each method in Visual Studio Code (see figure 4.1).

Figure 4.1. OmniSharp extension for Visual Studio Code can run unit tests

Although VS Code allows you to run an individual unit test, it doesn’t have a way to run all the tests in an integrated fashion. You can run all the tests by running the dotnet test command from the integrated terminal. In that case, the output from the tests goes to the Output pane in the editor.

Running tests from VS Code

See chapter 8 for information on how to use VS Code tasks to run tests.

Visual Studio for Mac runs tests through the Run menu. If you try to start a test project with or without debugging, VS for Mac will recognize that it’s a test project and collect the results from the tests in the Tests pane.

Visual Studio 2017 has more complete integration with .NET Core testing. Figure 4.2 shows a typical testing process.

Figure 4.2. Visual Studio 2017 test integration

4.7. When it’s impossible to prove all cases, use a theory

The test you wrote for WeekendRule is actually not a good example of a “fact.” There are many different inputs that you can use when testing this class, and it’s not possible to test them all. That’s why xUnit has theories. A theory is a test that’s true only for a particular set of data.

Add a few theories, as shown in the next listing.

Listing 4.6. WeekendRuleTest.cs using Theory
using System;
using BizDayCalc;
using Xunit;

namespace BizDayCalcTests
{
  public class WeekendRuleTest
  {
    [Fact]
    public void TestCheckIsBusinessDay()
    {
      var rule = new WeekendRule();
      Assert.True(rule.CheckIsBusinessDay(new DateTime(2016, 6, 27)));
      Assert.False(rule.CheckIsBusinessDay(new DateTime(2016, 6, 26)));
    }

    [Theory]                                                      1
    [InlineData("2016-06-27")] // Monday                          2
    [InlineData("2016-03-01")] // Tuesday
    [InlineData("2017-09-20")] // Wednesday
    [InlineData("2017-09-17")] // Sunday                          3
    {
      var rule = new WeekendRule();
      Assert.True(rule.CheckIsBusinessDay(DateTime.Parse(date)));
    }

    [Theory]
    [InlineData("2016-06-26")] // Sunday
    [InlineData("2016-11-12")] // Saturday
    public void IsNotBusinessDay(string date)
    {
      var rule = new WeekendRule();
      Assert.False(rule.CheckIsBusinessDay(DateTime.Parse(date)));
    }
  }
}

  • 1 Marks as a theory
  • 2 The data to pass to the test method
  • 3 This test method takes a parameter.

Run dotnet test to execute the tests. Notice that you now have six tests—each [InlineData] for your theory is an individual test. You can only test a small subset of possible dates, so these tests fit better as theories than as facts. Also note that one of these tests fails with an error, as you can see here:

Failed   BizDayCalcTests.WeekendRuleTest.IsBusinessDay(date: "2017-09-17")
Error Message:
 Assert.True() Failure
Expected: True
Actual:   False
Stack Trace:
   at BizDayCalcTests.WeekendRuleTest.IsBusinessDay(String date) in
   /chapter4/BusinessDays/BizDayCalcTests/WeekendRuleTest.cs:line 25

Notice that the failure message includes the parameters to the test method. This is very helpful for determining which inputs break the theory. In this case, you can remove the InlineData for "2017-09-17".

InlineData also allows you to specify multiple parameters, which can shorten the test code. The following listing shows an example of InlineData with multiple parameters.

Listing 4.7. WeekendRuleTest.cs using InlineData with multiple parameters
public class WeekendRuleTest
{
  [Theory]
  [InlineData(true,  "2016-06-27")]
  [InlineData(true,  "2016-03-01")]
  [InlineData(false, "2016-06-26")]
  [InlineData(false, "2016-11-12")]
  public void IsBusinessDay(bool expected, string date)
  {
    var rule = new WeekendRule();
    Assert.Equal(expected, rule.CheckIsBusinessDay(DateTime.Parse(date)));
  }
}

In the preceding code, you have only four tests specified, but already the attributes take up a lot of space. If you were to include InlineData to test every weekend in a given year, you’d end up with a large stack of attributes. In these cases, where you need many test cases for your theories, or if you want the test cases to generate data that isn’t easily specified statically in an attribute, use MemberData. The following listing shows an example of MemberData.

Listing 4.8. WeekendRuleTests.cs using [MemberData]
using System;
using System.Collections.Generic;                     1
using BizDayCalc;
using Xunit;

namespace BizDayCalcTests
{
  public class WeekendRuleTest
  {
    public static IEnumerable<object[]> Days {        2
      get {
        yield return new object[] {true,  new DateTime(2016,  6, 27)};
        yield return new object[] {true,  new DateTime(2016,  3,  1)};
        yield return new object[] {false, new DateTime(2016,  6, 26)};
        yield return new object[] {false, new DateTime(2016, 11, 12)};
      }
    }

    [Theory]
    [MemberData(nameof(Days))]
    public void TestCheckIsBusinessDay(bool expected, DateTime date)
    {
      var rule = new WeekendRule();
      Assert.Equal(expected, rule.CheckIsBusinessDay(date));
    }
  }
}

  • 1 Add for IEnumerable<>
  • 2 MemberData only works with static IEnumerable<object[]> members.

For those not familiar with C#

yield return can be used in C# properties that return IEnumerable (or IEnumerator). Instead of creating a List or array and returning it immediately, yield return lets you return each individual item as it’s accessed through the enumerator.

In the previous code listings, you had to pass a string in [InlineData] because a new DateTime isn’t a constant expression and therefore can’t be used as an argument to an attribute. With [MemberData] you can use a static property instead and create the DateTime objects inside. [MemberData] can only be used on static properties.

4.8. Shared context between tests

xUnit creates a new object of your test class for every test method it executes. That includes each invocation of a theory. This allows tests to be executed in any order and in parallel. xUnit will execute tests in random order.

Sometimes you have some setup or cleanup code that’s common to a set of tests. This is called shared context. xUnit has a few different approaches to shared context, depending on the level at which you want to share context.

4.8.1. Using the constructor for setup

The constructor of a test class can be used to share common setup code for all the test methods in a class. To see this in action, first create a new rule in the business-day calculator. In the BizDayCalc folder, create a new file called HolidayRule.cs with the following code.

Listing 4.9. Contents of HolidayRule.cs
using System;

namespace BizDayCalc
{
  public class HolidayRule : IRule
  {
    public static readonly int[,] USHolidays = {        1
      { 1, 1 },   // New Year's day
      { 7, 4 },   // Independence day
      { 12, 24 }, // Christmas eve
      { 12, 25 }  // Christmas day
    };

    public bool CheckIsBusinessDay(DateTime date)
    {
      for (int day = 0; day <=
        USHolidays.GetUpperBound(0); day++)             2
      {
        if (date.Month == USHolidays[day, 0] &&
            date.Day   == USHolidays[day, 1])
            return false;
      }
      return true;
    }
  }
}

  • 1 A two-dimensional array
  • 2 GetUpperBound gets the highest index in the given dimension.

This is a new rule that adds U.S. holidays that are the same from year to year.

Instead of writing tests against the HolidayRule directly, use the Calculator class in your test. In the BizDayCalcTests folder, create a new file called USHolidayTest.cs with the following code.

Listing 4.10. Contents of USHolidayTest.cs
using System;
using System.Collections.Generic;
using BizDayCalc;
using Xunit;

namespace BizDayCalcTests
{
  public class USHolidayTest
  {
    public static IEnumerable<object[]> Holidays {
      get {
        yield return new object[] { new DateTime(2016, 1, 1) };
        yield return new object[] { new DateTime(2016, 7, 4) };
        yield return new object[] { new DateTime(2016, 12, 24) };
        yield return new object[] { new DateTime(2016, 12, 25) };
      }
    }

    private Calculator calculator;

    public USHolidayTest()                          1
    {
      calculator = new Calculator();
      calculator.AddRule(new HolidayRule());
    }

    [Theory]
    [MemberData(nameof(Holidays))]
    public void TestHolidays(DateTime date)
    {
      Assert.False(calculator.IsBusinessDay(date));
    }

    [Theory]
    [InlineData("2016-02-28")]
    [InlineData("2016-01-02")]
    public void TestNonHolidays(string date)
    {
      Assert.True(calculator.IsBusinessDay(DateTime.Parse(date)));
    }
  }
}

  • 1 The constructor creates the context.

In the preceding test, the calculator field is instantiated in the constructor and used by both test methods. The TestHolidays theory will execute four times, and the TestNonHolidays theory will execute twice. A USHolidayTest object will be created for each test execution, so the constructor will be called six times. You can verify this by placing a Console.WriteLine in the constructor, as follows.

Listing 4.11. USHolidayTest with Console.WriteLines
public USHolidayTest()
{
  calculator = new Calculator();
  calculator.AddRule(new HolidayRule());
  Console.WriteLine("In USHolidayTest constructor");
}

[Theory]
[InlineData("2016-02-28")]
[InlineData("2016-01-02")]
public void TestNonHolidays(string date)
{
  Assert.True(calculator.IsBusinessDay(DateTime.Parse(date)));
  Console.WriteLine($"In TestNonHolidays {date}");             1
}

[Theory]
[MemberData(nameof(Holidays))]
public void TestHolidays(DateTime date)
{
  Assert.False(calculator.IsBusinessDay(date));
  Console.WriteLine(
    $"In TestHolidays {date:yyyy-MM-dd}");                     2
}

  • 1 $“... {date}” is a shortcut for inserting values into strings.
  • 2 :yyyy-MM-dd is used to format the DateTime.

In the output you’ll see that the constructor is called before each test method invocation. Here’s an example of this output:

In USHolidayTest constructor
In TestNonHolidays 2016-02-28
In USHolidayTest constructor
In TestNonHolidays 2016-01-02
In USHolidayTest constructor
In TestHolidays 2016-01-01
In USHolidayTest constructor
In TestHolidays 2016-07-04
In USHolidayTest constructor
In TestHolidays 2016-12-24
In USHolidayTest constructor
In TestHolidays 2016-12-25

4.8.2. Using Dispose for cleanup

Just as common setup code can be added to the constructor, common cleanup code can be added to the Dispose method. xUnit uses the dispose pattern because it’s a well-known .NET pattern that’s more intuitive than creating an explicit teardown method. If you’re already familiar with the dispose pattern, skip the next section.

The dispose pattern

The dispose pattern is a common .NET pattern used to clean up resources. .NET has a garbage collector built in that will free memory that you’re no longer using. However, there are cases where you need to free other resources explicitly, such as closing file handles or network sockets. Consider the following code, taken from chapter 3.

Listing 4.12. CSV parsing code from chapter 3
public static void Main(string[] args)
{
  var sr = new StreamReader(new FileStream("Marvel.csv",
    FileMode.Open));
  var csvReader = new CsvReader(sr);
  foreach (var line in csvReader.Lines)
    Console.WriteLine(line.First(p => p.Key == "Title").Value);
}

This code creates a new FileStream that’s passed into a StreamReader. It opens a file handle, an operating system resource for manipulating files, but it never explicitly closes it. The file handle will be closed when the process ends, so it’s not an issue for this scenario. But if your program is opening many files, it should explicitly close them.

The code from listing 4.12 can be modified as follows.

Listing 4.13. CSV parsing code from chapter 3 modified to close the CSV file
public static void Main(string[] args)
{
  using (var sr = new StreamReader(new FileStream("Marvel.csv",
    FileMode.Open)))
  {
    var csvReader = new CsvReader(sr);
    foreach (var line in csvReader.Lines)
      Console.WriteLine(line.First(p => p.Key == "Title").Value);
  }
}

The using statement is a helpful C# tool for indicating that the sr object is “disposable” and explicitly defines when it should be disposed of. The using statement is nice, because if an exception is thrown inside a using block, the disposal will still be performed.

A using can only be used on a type that implements the IDisposable interface. This interface has one method: Dispose(). Note that although using will explicitly call the Dispose method, the .NET garbage collector won’t. The specifics of garbage collection and disposal are beyond the scope of this book.

Using the dispose pattern in xUnit unit tests

The business-day calculator library doesn’t need any cleanup code in its unit tests. But if you were writing unit tests for the CSV parser created in chapter 3, you might want some cleanup code. The following listing shows how you could use the dispose pattern in a unit test of the CSV parser library.

Listing 4.14. CSV parser library unit test using dispose pattern
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using CsvParser;
using Xunit;

namespace CsvParserTests
{
  public class CsvReaderTest : IDisposable          1
  {
    private StreamReader streamReader;
    private CsvReader csvReader;

    public CsvReaderTest()
    {
      streamReader = new StreamReader(new FileStream("Marvel.csv",
        FileMode.Open));
      csvReader = new CsvReader(streamReader);
    }

    public void Dispose()
    {
      streamReader.Dispose();                       2
    }

    [Fact]
    public void VerifyNumberOfLines()
    {
      Assert.Equal(7, csvReader.Lines.Count());     3
    }
  }
}

  • 1 Implements the IDisposable interface
  • 2 Your Dispose must call the StreamReader’s Dispose.
  • 3 Count() is a LINQ extension method.

Here you open a file in the constructor so that it can be used by each test method. You then close that file in the Dispose method, which will keep you from leaking open file handles. Just as the constructor is called before each test method invocation, the Dispose method is called after each test method invocation.

4.8.3. Sharing context with class fixtures

The constructor in a test class lets you share the setup code for each of the test methods. But there are cases where you want a setup operation to be performed once and reused for the entire test class. xUnit’s solution to this is called a class fixture.

The business-day calculator allows you to add many rules, so you want to test the case where many rules are applied. It might be an expensive operation to create a calculator that has all the rules if you have to load those rules from a file or a database. In this case, you can create a class fixture so that the setup and cleanup operations are only performed once for the whole test class.

The business-day calculator will operate differently depending on the region it’s in. For example, the weekend rule in the United States only needs to check if the day of the week is Saturday or Sunday. But in China, weekends can be moved adjacent to national holidays. A region would therefore have a collection of applicable rules, much like your calculator.

The following listing shows an example of a class fixture that tests the rules for the United States region.

Listing 4.15. Example class fixture for the business-day calculator
using BizDayCalc;

namespace BizDayCalcTests
{
  public class USRegionFixture
  {
    public Calculator Calc { get; private set; }     1

    public USRegionFixture()
    {
      Calc = new Calculator();
      Calc.AddRule(new WeekendRule());
      Calc.AddRule(new HolidayRule());
    }
  }
}

  • 1 A property with a public getter and private setter

Now create a test that uses the preceding class fixture. Create a new file named USRegionTest.cs and copy the code from USHolidayTest. The following listing shows how to modify the test code to use the fixture.

Listing 4.16. Contents of USRegionTest.cs
using System;
using BizDayCalc;
using Xunit;

namespace BizDayCalcTests
{
  public class USRegionTest
    : IClassFixture<USRegionFixture>                 1
  {
    private USRegionFixture fixture;

    public USRegionTest(USRegionFixture fixture)     2
    {
      this.fixture = fixture;
    }

    [Theory]
    [InlineData("2016-01-01")]
    [InlineData("2016-12-25")]
    public void TestHolidays(string date)
    {
      Assert.False(fixture.Calc.IsBusinessDay(       3
        DateTime.Parse(date)));
    }

    [Theory]
    [InlineData("2016-02-29")]
    [InlineData("2016-01-04")]
    public void TestNonHolidays(string date)
    {
      Assert.True(fixture.Calc.IsBusinessDay(
        DateTime.Parse(date)));
    }
  }
}

  • 1 Tells xUnit that this test uses a class fixture
  • 2 xUnit creates the object and passes it to the test’s constructor.
  • 3 Using the Calculator object from the fixture

To identify exactly how often the class fixture object is created, add a Console.WriteLine to the fixture’s constructor (be sure to add using System at the top). You should see it constructed only once, before any of the tests execute.

Class fixture cleanup

You can add cleanup code to class fixtures by using the same dispose pattern described earlier in this chapter.

4.8.4. Sharing context with collection fixtures

Class fixtures share context for one test class, but you may instead need to share context among multiple test classes. The way to handle this in xUnit is with a collection fixture. Creating a collection fixture is simple enough that I always create one when creating a class fixture.

The first step is to add a second, empty class for the collection fixture that uses the class fixture. The following listing shows how to do that with the USRegionFixture class.

Listing 4.17. USRegionFixture with a collection fixture
using BizDayCalc;
using Xunit;

namespace BizDayCalcTests
{
  public class USRegionFixture
  {
    public Calculator Calc { get; private set; }

    public USRegionFixture()
    {
      Calc = new Calculator();
      Calc.AddRule(new WeekendRule());
      Calc.AddRule(new HolidayRule());
    }
  }

  [CollectionDefinition("US region collection")]      1
  public class USRegionCollection
    : ICollectionFixture<USRegionFixture>             2
  {
  }
}

  • 1 The name of the collection
  • 2 The class fixture is what the test classes will use.

To use the collection fixture in a test class, you need to refer to the collection by name. The test class doesn’t need to implement the IClassFixture interface. Besides those two changes, the test class works just as if you were using a class fixture.

The following listing shows the USRegionTest class modified to use the collection fixture.

Listing 4.18. USRegionTest with collection fixture
[Collection("US region collection")]                 1
public class USRegionTest                            2
{
  private USRegionFixture fixture;

  public USRegionTest(USRegionFixture fixture)
  {
    this.fixture = fixture;
  }
  ...                                                3
}

  • 1 Refers to the collection by name
  • 2 Test class no longer implements interface
  • 3 Remainder of the class is the same

The collection fixture class doesn’t have any setup or cleanup code of its own. That’s done in the class fixture.

4.9. Getting output from xUnit tests

Using Console.WriteLine to get output from a test is problematic. xUnit typically runs tests in parallel, so console output will overlap between tests. Also, if you’re using a build automation system or IDE like Visual Studio, these tools will typically provide the output from the test cases that fail. If a test passes, you don’t normally want to see any output from it, but if it fails, the output is useful in diagnosing the failure. Console.WriteLine won’t work in these situations.

There’s a better way to write test output. xUnit provides an interface called ITestOutputHelper that has a WriteLine method on it that you can use to write test output. Try this out on the USRegionTest test class.

First, modify the BizDayCalcTests.csproj file to disable the other two tests, as follows.

Listing 4.19. Exclude other tests besides USRegionTest.cs
<ItemGroup>
  <Compile Remove="USHolidayTest.cs" />
  <Compile Remove="WeekendRuleTest.cs" />
</ItemGroup>

Now modify USRegionTest to use ITestOutputHelper.

Listing 4.20. USRegionTest using ITestOutputHelper
using System;
using BizDayCalc;
using Xunit;
using Xunit.Abstractions;                             1

namespace BizDayCalcTests
{
  [Collection("US region collection")]
  public class USRegionTest
  {
    private readonly USRegionFixture fixture;
    private readonly ITestOutputHelper output;

    public USRegionTest(
      USRegionFixture fixture,
      ITestOutputHelper output)                          2
    {
      this.fixture = fixture;
      this.output = output;
    }

    [Theory]
    [InlineData("2016-01-01")]
    [InlineData("2016-12-25")]
    public void TestHolidays(string date)
    {
      output.WriteLine($@"TestHolidays(""{date}"")");    3
      Assert.False(fixture.Calc.IsBusinessDay(
        DateTime.Parse(date)));
    }

    [Theory]
    [InlineData("2016-02-29")]
    [InlineData("2016-01-04")]
    public void TestNonHolidays(string date)
    {
      output.WriteLine($@"TestNonHolidays(""{date}"")");
      Assert.True(fixture.Calc.IsBusinessDay(
        DateTime.Parse(date)));
    }
  }
}

  • 1 ITestOutputHelper is in Xunit.Abstractions.
  • 2 xUnit will provide the ITestOutputHelper object.
  • 3 $@“...” combines string replacement with verbatim

When you have an ITestOutputHelper parameter on your constructor, xUnit will detect that and provide an implementation. It doesn’t matter how many other parameters are on the constructor or in what order they appear. The output helper will know automatically which test you’re running and will correlate the output written for that test.

If you run dotnet test, you won’t see the test output because all the tests pass. To see the output, try changing Assert.False to Assert.True in TestHolidays, which should produce output like the following.

Listing 4.21. Test output appears only when tests fail
BizDayCalcTests.USRegionTest.TestHolidays(date: "2016-01-01") [FAIL]
  Assert.True() Failure
  Expected: True
  Actual:   False
  Stack Trace:
    C:devBusinessDaysBizDayCalcTestsUSRegionTest.cs(28,0):
      at BizDayCalcTests.USRegionTest.TestHolidays(String date)
  Output:
    TestHolidays("2016-01-01")                                         1
  Assert.True() Failure
BizDayCalcTests.USRegionTest.TestHolidays(date: "2016-12-25") [FAIL]
  Expected: True
  Actual:   False
  Stack Trace:
    C:devBusinessDaysBizDayCalcTestsUSRegionTest.cs(28,0):
      at BizDayCalcTests.USRegionTest.TestHolidays(String date)
  Output:
    TestHolidays("2016-12-25")                                         1

  • 1 Logged test output

4.10. Traits

Traits allow you to assign any number of properties to a test. You can use this to organize tests into categories so you can exercise specific areas of your code. The following listing shows how you can apply traits to the USRegionTest class.

Listing 4.22. USRegionTest.cs with traits
[Theory]
[InlineData("2016-01-01")]
[InlineData("2016-12-15")]
[Trait("Holiday", "true")]                                   1
public void TestHolidays(string date)
{
  output.WriteLine($@"TestHolidays(""{date}"")");
  Assert.False(fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
}

[Theory]
[InlineData("2016-02-28")]
[InlineData("2016-01-02")]
[Trait("Holiday", "false")]                                  2
public void TestNonHolidays(string date)
{
  output.WriteLine($@"TestNonHolidays(""{date}"")");
  Assert.True(fixture.Calc.IsBusinessDay(DateTime.Parse(date)));
}

  • 1 Trait is a name/value pair.
  • 2 Key and value are both strings.

With the traits set, you can specify command-line parameters to xUnit for the traits you want, as follows.

dotnet test --filter Holiday=true

Total tests: 4. Passed: 4. Failed: 0. Skipped: 0.

See appendix B for more information about specifying traits and other command-line options.

Additional resources

To learn more about xUnit, try these resources:

Summary

In this chapter you learned about unit testing in .NET Core with xUnit, and we covered some of xUnit’s features. These key concepts were covered:

  • Facts and theories
  • Providing theory data in different ways
  • Sharing context between test cases
  • Logging and viewing data from unit tests

These are some important techniques to remember from this chapter:

  • Use the dispose pattern to clean up resources.
  • The dispose pattern also works on class fixtures.
  • Use ITestOutputHelper to correlate logs with individual tests.

Unit testing is a great way to improve the reliability of your code. Having a suite of tests helps you build features with confidence and identify breaking changes as your code evolves. An essential part of any development platform is the ability to unit test.

In this chapter, we focused on xUnit because it’s supported on .NET Core and is popular with early adopters. In the next few chapters, you’ll do more interesting things with .NET Core while using your xUnit testing skills.

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

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