Practical .NET

Providing Constant Context for .NET Core Tests

The new xUnit Testing Framework that's part of .NET Core works differently from the testing frameworks you're used to. During a test run, the other Visual Studio frameworks instantiate your test class once and then call the test methods in your class. xUnit, on the other hand, recreates your test class for each test method call.

This both ensures that each of your tests is completely isolated from any other test and makes it easier for xUnit to run tests in parallel. However, if you've used the ClassInitialize, ClassCleanup, TestInitialize or TestCleanup attributes in other testing environments, you're going to be unhappy with the fallout from this new paradigm: Those attributes are gone from xUnit.

One Context for Each Test
The good news here is that if you use TestInitialize or TestCleanup, that functionality not only still exists, it fits into xUnit's process in a very natural way. To replicate TestInitialize functionality, all you have to do is put your initialization code in your test class's constructor. To replicate TestCleanup, just have your test class implement the IDisposable interface and put your code in the resulting Dispose method. Your constructor and Dispose method will be run automatically as xUnit creates and destroys your test class (in fact, the automatic destruction of your test class after every test method may make putting any code in the Dispose method unnecessary).

Here's a typical example of a test class with a constructor and a Dispose method:

public class MyTests : IDisposable
{
  public MyTests()
  {
    // ... generate per-test data ... 
  }
  public void Dispose()
  {
    // ... destroy per-test data ... 
  }

  [Fact]
  public void TestCustomer()
  {

One Context to Rule Them All
The bad news here is what doesn't fit into this paradigm: methods that initialize the environment just once for all the tests (that is, methods you'd decorate with the ClassInitialize or ClassCleanup attributes in other frameworks). Fortunately, here too, xUnit provides alternatives to the ClassInitialize and ClassCleanup attributes that provide similar functionality. xUnit even throws in some additional features that weren't possible with the ClassInitialize/Cleanup attributes.

The xUnit authors define the use case for the ClassInitialize/Cleanup attributes as creating a single test context, shared among all the tests in the class and cleaned up after all the tests in the class have finished.

In xUnit, you implement this use case by creating a fixture class that provides/creates whatever resources your tests need (the xUnit authors refer to this class as a "test fixture class"). This test fixture class can, like your test class, implement the IDisposable interface. And, like your test class, you'll put your test initialization code in your fixture class's constructor and any cleanup code in its Dispose method.

A fixture class might be as simple as this:

public class CreateCustomerData : IDisposable
{
  public CreateCustomerData()
  {
    // ... insert data into the database ...
  }

  public void Dispose()
  {
    // ... delete data from the database ...
  }
}

To use this fixture class with a test class, you add the IClassFixture interface to your test class, referencing your fixture class. When xUnit goes to run your tests, it will instantiate your fixture class just once in a test run.

Code like this would use my example fixture class:

public class MyTests : IClassFixture
{

Exploiting the Fixture Class
Your fixture class doesn't have to be limited to a constructor and Dispose method. Going beyond what the ClassInitialize method delivered, your fixture class can provide controlled access to resources by adding methods and properties to your fixture class. To access the members of your fixture class, you need to add a constructor to your test class that accepts your fixture class. After instantiating your fixture class, xUnit will automatically pass your fixture class to your test class's constructor. Typically, you'll store that class in a field so your test methods can access any members you've put on your fixture class.

As an example, this fixture class creates a collection of Customer objects to be used by my tests and exposes them through a property called Customers:

public class CreateCustomerData : IDisposable
{
  public List Customers {get; set;}

  public CreateCustomerData()
  {
    // ... add Customer objects to the Customers collection ...
  }

This test class accepts the CreateCustomerData class and stores it in a field to be used by tests in the class:

public class MyCustomerTests : IClassFixture
{  
  private CreateCustomerData custData {get; set;}

  public MyCustomerTests(CreateCustomerData custData)
  {
    this.custData = custData;
  }

  [Fact]
  public void BillCustomers()
  {
    CustomerBilling cb = new CustomerBilling();
    var bills = cb.BillMultiple(custData.Customers);

One Collection to Rule Them All
This enables the possibility of using your fixture class in several different test classes, making the fixture class more useful than a method decorated with the ClassInitialize method. However, if you use the code I've showed so far, that fixture class will be not be shared between the test classes -- each test class will get its own separate copy of the fixture class. That may be fine, of course, but you can have all the tests in all of your test classes use the same fixture.

To share a single instance of the fixture class among multiple test classes, you must create a dummy class that both flags your test fixture class as a shareable object and assigns that shared object a unique name. You just need to do two things when creating the class. First, you must have this class implement the ICollectionFixture interface, referencing your fixture class (this interface has no members, so implementing the interface is easy). Second, you must assign this class a name by decorating it with the CollectionDefinition attribute.

This example establishes my CreateCustomerData class as a shareable fixture with the name Customer Data:

[CollectionDefinition("Customer Data")]
public class CustomerCollection : ICollectionFixture
{
  // ... no members required ... 
}

Now, to use your fixture class in any test class, you decorate your test class with the Collection attribute, passing the name you assigned to your fixture class in its CollectionDefinition attribute. As you did with the ICollectionFixture interface, if you want to access the fixture within your tests, you must create a constructor to accept your fixture class(es). If you're going to use this feature to invoke your fixture class, make sure you don't also use ICollectionFixture to invoke it on the same test class.

As an example, both these test classes accept my fixture object and move it to a field. Because I've shared the class through the CollectionDefinition and Collection attributes, my fixture class will be created only once:

[Collection("Customer Data")]
public class MyCustomerTests
{
  CreateCustomerData custData;
  public MyCustomerTests(CreateCustomerData custData)
  {
    this.custData = custData;
  }
}

[Collection("Customer Data")]
public class MyTransactionTests
{
  CreateCustomerData custData;
  public MyTransactionTests(CreateCustomerData custData)
  {
    this.custData = custData;
  }
}

While xUnit has done away with the ClassInitialize, ClassCleanup, TestInitialize and TestCleanup attributes, it hasn't abandoned you. Instead, xUnit offers alternatives that are more object-oriented, more powerful and more convenient. It's hard to complain.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube