Unit testing .Net Framework/.Net Core using xUnit

Introduction

xUnit.net is a free, open source, community-focused unit testing tool for the .NET Framework. xUnit.net is the latest technology for unit testing C#, F#, VB.NET and other .NET languages. xUnit.net works with ReSharper, CodeRush, TestDriven.NET and Xamarin.

If you’re developing a .NET-based application, Microsoft provides MSTest framework that has excellent integration with Visual Studio. When it’s time to execute these unit tests on a build engine, the MSTest execution engine requires an installation of Visual Studio to be available on the build server. You can choose any build server, but Visual Studio must be installed on it. You won’t always have complete access to the build server to install the tools that you need. Here, xUnit comes handy!

Advantages of xUnit over MSTest

The xUnit tool has gained popularity over MSTest for following reasons:

  • It provides support for parameterized tests using the Theory attribute whereas MSTest doesn’t provide such a feature out of the box.
  • Tests in xUnit don’t require a separate Visual Studio Test Project as mandated by MSTest.
  • Prioritizing or sequencing xUnit tests is simple (with the TestPriority attribute) unlike in MSTest framework, which requires a unit test settings file.
  • It supports fluent assertions like Assert.Throws<> instead of ExpectedException attribute in MSTest framework.
  • It can be referenced in any Visual Studio Project as a NuGet package.
  • Unlike MSTest, xUnit doesn’t require any additional tools to be installed on the build server.

Writing test cases using xUnit

In this section we will demonstrate how we can use xUnit for .Net Framework and write some basic test cases and run those test cases in console mode and visual studio. We will also explore options to initialize the context and share the context between test cases.

Basic test

Create a new class library project in Visual studio, File->New->Project→Class Library(.Net Framework). Make sure you select the .Net framework version 4.5.2 and above. Once the project is created, right click on the project and choose Manage Nuget Packages. Browse for the package 'xunit' and install the same to your project.

Now add a new class such as 'BasicTest.cs' and add the following code snippet


using Xunit;
namespace xUnitTestingDemo
{
    public class BasicTest
    {
        int Add(int x, int y)
        {
            return x + y;
        }
 
        [Fact]
        public void PassingTest()
        {
            Assert.Equal(4, Add(2, 2));
        }
 
        [Fact]
        public void FailingTest()
        {
            Assert.Equal(5, Add(2, 2));
        }
    }
}

Build the code and ensure there are no errors.

Fact is an attribute which indicates the the method to be tested. xUnit uses the Fact attribute to locate the method and executes through reflection. Note, we don't declare any attribute to define the calls is a test class. xUnit minimizes the use of attributes.

Running test using console

Now that we have the test project ready we can execute the test cases using console mode. To run the project in console, we need to add a nuget library 'xunit.runner.console'. Include the library from nuget package manager as described above. Once it is added rebuild the project.

To execute the test cases open the command prompt(to the solution folder) and execute the following command, 'packages\xunit.runner.console.2.4.1\tools\net472\xunit.console xUnitTestingDemo\bin\Debug\xUnitTestingDemo.dll'.

Note: Substitute the project name of yours instead of 'xUnitTestingDemo'.

You will see the similar output as shown below,


Running test using visual studio

Install the package 'xunit.runner.visualstudio' from nuget package manager as described above. Every time when you build a project visual studio will automatically detect the test cases. All the test cases are visible in 'Test Explorer' window.

You will see the test cases as shown below,



Test Initialization and context sharing

It is common for unit test classes to share setup and cleanup code (often called "test context"). xUnit.net offers the following methods for sharing this setup and cleanup code.

Constructor and Dispose

Constructor and dispose will be used when you want a clean test context for every test (sharing the setup and cleanup code, without sharing the object instance).

xUnit creates a new instance of the test class for every test that is run, so any code which is placed into the constructor of the test class will be run for every single test. This makes the constructor a convenient place to put reusable context setup code where you want to share the code without sharing object instances.

Here is a sample example,


public class StackTests : IDisposable
{
    Stack<int> stack;
 
    public StackTests()
    {
        stack = new Stack<int>();
    }
 
    public void Dispose()
    {
        stack.Dispose();
    }
 
    [Fact]
    public void WithNoItems_CountShouldReturnZero()
    {
        var count = stack.Count;
 
        Assert.Equal(0, count);
    }
 
    [Fact]
    public void AfterPushingItem_CountShouldReturnOne()
    {
        stack.Push(42);
 
        var count = stack.Count;
 
        Assert.Equal(1, count);
    }
}


Unlike MSTest we don't declare the 'TestInitialize' separately. Since the constructor is the class initialization, xUnit uses the power of constructor and initializes the context. Similarly the dispose method will clean up all the resources.

Class Fixtures

Class fixtures are used when you want to create a single test context and share it among all the tests in the class, and have it cleaned up after all the tests in the class have finished.

Sometimes test context creation and cleanup can be very expensive. If you were to run the creation and cleanup code during every test, it might make the tests slower than you want. You can use the class fixture feature of xUnit to share a single object instance among all tests in a test class. This means the context is created only once for a class instead of each test cases seperately.

To use class fixtures, you need to take the following steps:

·        Create the fixture class, and put the startup code in the fixture class constructor.

·        If the fixture class needs to perform cleanup, implement IDisposable on the fixture class, and put the cleanup code in the Dispose() method.

·        Add IClassFixture<> to the test class.

·        If the test class needs access to the fixture instance, add it as a constructor argument, and it will be provided automatically.

Here is a sample example,


public class DatabaseFixture : IDisposable
{
    public DatabaseFixture()
    {
        Db = new SqlConnection("MyConnectionString");
 
        // ... initialize data in the test database ...
    }
 
    public void Dispose()
    {
        // ... clean up test data from the database ...
    }
 
    public SqlConnection Db { get; private set; }
}
 
public class MyDatabaseTests : IClassFixture<DatabaseFixture>
{
    DatabaseFixture fixture;
 
    public MyDatabaseTests(DatabaseFixture fixture)
    {
        this.fixture = fixture;
    }
 
    // ... write tests, using fixture.Db to get access to the SQL Server ...
}

Here xUnit crates the object DatabaseFixture and injects it thought the constructor of test class. So the context need not to be created every time for each tests.

Collection Fixtures

Collection fixtures when you want to create a single test context and share it among tests in several test classes, and have it cleaned up after all the tests in the test classes have finished.

Sometimes you will want to share a fixture object among multiple test classes. For example, you may want to initialize a database with a set of test data, and then leave that test data in place for use by multiple test classes.

To use collection fixtures, you need to take the following steps:

·        Create the fixture class, and put the startup code in the fixture class constructor.

·        If the fixture class needs to perform cleanup, implement IDisposable on the fixture class, and put the cleanup code in the Dispose() method.

·        Create the collection definition class, decorating it with the [CollectionDefinition] attribute, giving it a unique name that will identify the test collection.

·        Add ICollectionFixture<> to the collection definition class.

·        Add the [Collection] attribute to all the test classes that will be part of the collection, using the unique name you provided to the test collection definition class's [CollectionDefinition] attribute.

·        If the test classes need access to the fixture instance, add it as a constructor argument, and it will be provided automatically.

Here is an example,

public class DatabaseFixture : IDisposable
{
    public DatabaseFixture()
    {
        Db = new SqlConnection("MyConnectionString");
 
        // ... initialize data in the test database ...
    }
 
    public void Dispose()
    {
        // ... clean up test data from the database ...
    }
 
    public SqlConnection Db { get; private set; }
}
 
[CollectionDefinition("Database collection")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture>
{
    // This class has no code, and is never created. Its purpose is simply
    // to be the place to apply [CollectionDefinition] and all the
    // ICollectionFixture<> interfaces.
}
 
[Collection("Database collection")]
public class DatabaseTestClass1
{
    DatabaseFixture fixture;
 
    public DatabaseTestClass1(DatabaseFixture fixture)
    {
        this.fixture = fixture;
    }
}
 
[Collection("Database collection")]
public class DatabaseTestClass2
{
    // ...
}


xUnit treats collection fixtures in much the same way as class fixtures, except that the lifetime of a collection fixture object is longer: it is created before any tests are run in any of the test classes in the collection, and will not be cleaned up until all test classes in the collection have finished running.

Working with xUnit Theory

Theories are tests which are only true for a particular set of data. xUnit Theory depends on set of parameters and its data, our test will pass for some set of data and not the others. We have a theory which postulate that with this set of data, this will happen.

There are three ways we can formulate a theory.

Inline Data

This is a simplest form of testing our theory with data. Let us see an example,

using Xunit;
 
namespace xUnitTestingDemo
{
    public class InlineDataTest
    {
        int Add(int x, int y)
        {
            return x + y;
        }
 
        [Theory]
        [InlineData(4, 2, 2)]
        [InlineData(10, 6, 4)]
        [InlineData(25, 20, 10)]
        public void PassingTest(int expected, int opr1 , int opr2)
        {
            Assert.Equal(expected, Add(opr1, opr2));
        }
    }
}


As you see above, we provide some values in InlineData and xUnit will create two tests and every time populates the test case arguments with what we’ve passed into InlineData.

Class Data

ClassData is another attribute that we can use with our theory, with ClassData we have more flexibility and less clutter

using Xunit;
using System.Collections.Generic;
using System.Collections;
 
namespace xUnitTestingDemo
{
    public class TestDataGenerator : IEnumerable<object[]>
    {
        private readonly List<object[]> _data = new List<object[]>
        {
            new object[]{4, 2, 2},
            new object[]{10, 6, 4},
            new object[]{25, 20, 10},
        };
 
        public IEnumerator<object[]> GetEnumerator() => _data.GetEnumerator();
 
        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
 
    public class ClassDataTest
    {
        int Add(int x, int y)
        {
            return x + y;
        }
 
        [Theory]
        [ClassData(typeof(TestDataGenerator))]
        public void PassingTest(int expected, int opr1, int opr2)
        {
            Assert.Equal(expected, Add(opr1, opr2));
        }
    }
}


Here we have created a class that inherits from IEnumerable<object[]>, note that it has to be an object, otherwise xUnit will throws an error. Next we create a private list of object that intend to pass to our theory and finally we implemented the GetEnumerator method with piggybacking on our list Enumerator. Now we can pass our TestDataGenerator class to ClassData attribute and the returned data form that class will populate the test case’s parameters.

Member Data

MemberData gives us the same flexibility but without the need for a class.

using Xunit;
using System.Collections.Generic;
using System.Collections;
 
namespace xUnitTestingDemo
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }
 
    public class DataGenerator
    {
        public static IEnumerable<object[]> GetPersonData()
        {
            yield return new object[]
            {
                new Person{Name = "Arun", Age = 35 },
                new Person{Name = "Venkatesan", Age = 20}
            };
        }
    }
    public class MemberDataTest
    {
        public bool IsAgeAboveTwentyFive(Person person)
        {
            return person.Age > 25;
        }
 
        [Theory]
        [MemberData(nameof(DataGenerator.GetPersonData), MemberType = typeof(DataGenerator))]
        public void IfPersonAgeIsMoreThanTwenttyFive(Person a, Person b)
        {
            Assert.True(IsAgeAboveTwentyFive(a));
            Assert.True(IsAgeAboveTwentyFive(b));
        }
    }
}


The [MemberData] attribute can load data from an IEnnumerable<object[]> property on the test class. The xUnit analyzers will pick up any issues with your configuration, such as missing properties, or using properties that return invalid types.

Handling exception in xUnit

When writing tests it is sometimes useful to check that the correct exceptions are thrown at the expected time.

Let look at an example,

using System;
using Xunit;
 
namespace xUnitTestingDemo
{
    public class TemperatureSensor
    {
        bool isInitialized;
 
        public void Initialize()
        {
            isInitialized = true;
        }
 
        public int ReadCurrentTemperature()
        {
            if (!isInitialized) 
                throw new InvalidOperationException("Cannot read temperature before initializing.");
 
            return 42; //simulation for demo
        }
    }
 
    public class ExceptionHandlingTest
    {
        [Fact]
        public void ReadTemperature()
        {
            var sensor = new TemperatureSensor();
            sensor.Initialize();
            Assert.Equal(42, sensor.ReadCurrentTemperature());
        }
 
        [Fact]
        public void ErrorIfReadingBeforeInitialized()
        {
            var sensor = new TemperatureSensor();
            Assert.Throws<InvalidOperationException>(() => sensor.ReadCurrentTemperature());
        }
 
        [Fact]
        public void ErrorIfReadingBeforeInitializedCaptureException()
        {
            var sensor = new TemperatureSensor();
            var ex = Assert.Throws<InvalidOperationException>(() => sensor.ReadCurrentTemperature());
            Assert.Equal("Cannot read temperature before initializing.", ex.Message);
        }
    }
}


The first test case is the happy path, where the test case evaluate to correct value. The second test case, expected exception is tested using, Assert.Throws<> and exception type as generic parameter. The third test case captures the exception in a variable for further assertions.

References

To get more documentation and resources on xUnit, refer the following websites,

  1. xUnit home page: https://xunit.net/
  2. Github: https://github.com/xunit/xunit

Comments

Popular posts from this blog

Debugging and Testing Helm Charts Using VS Code

Handle Multipart Contents in Asp.Net Core

Validate appsettings in ASP.net Core using FluentValidation