Unit Testing with xUnit in .NET 8

Unit Testing with xUnit

Overview

The xUnit testing framework is a popular choice in .NET 8 for writing and executing unit tests in C# and is an integral part of software development. We will cover essential concepts and provide code examples in this comprehensive guide as we explore the fundamentals of unit testing with xUnit.

Prerequisites

To follow this article, ensure we have the following installed.

  • .NET SDK 8.0 or later (Download .NET SDK)
  • Visual Studio Code or Visual Studio (optional but recommended)

Creating a Sample Project

The first step is to create a simple C# console application and its corresponding unit test project using xUnit.

# Create a new console application
dotnet new console -n ZiggyRafiqConsoleApp

# Create a new xUnit test project
dotnet new xunit -n ZiggyRafiqConsoleApp.Tests

Write Our Console App

For the Program.cs we just printed my name Ziggy Rafiq and this article title. As the code example below shows us.

Console.WriteLine("Hi and Welcome to Ziggy Rafiq Article on Unit Testing with xUnit in .NET 8: A Comprehensive Guide");

Create Our Calculator Class

We just create a simple Calculator class with simple functions such as Add, Subtract, Multiply, Divide, and the key one Dispose of as the code example below.


namespace ZiggyRafiqConsoleApp
{
    public class Calculator : IDisposable
    {
        // Add method
        public int Add(int a, int b)
        {
            return a + b;
        }

        // Subtract method
        public int Subtract(int a, int b)
        {
            return a - b;
        }

        // Multiply method
        public int Multiply(int a, int b)
        {
            return a * b;
        }

        // Divide method
        public double Divide(int a, int b)
        {
            if (b == 0)
            {
                throw new ArgumentException("Cannot divide by zero.");
            }

            return (double)a / b;
        }

        private bool disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                {
                    // Dispose of managed resources here go here
                }

                // Dispose of unmanaged resources goes here , if any
                disposed = true;
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        ~Calculator()
        {
            Dispose(false);
        }
    }
}

Data Source

We will create two files for our Data Source one is an interface and the other is a class as code example below.

namespace ZiggyRafiqConsoleApp
{
    public interface IDataSource
    {
        string GetData();
    }
}
namespace ZiggyRafiqConsoleApp
{
    public class DataService
    {
        private readonly IDataSource _dataSource;

        public DataService(IDataSource dataSource)
        {
            _dataSource = dataSource;
        }

        public string ProcessData()
        {
            return _dataSource.GetData().ToUpper();
        }
    }
}

Now we are all set to create our Unit Tests 😊

Writing Our First Test

Our preferred code editor should open the ZiggyRafiqConsoleApp.Tests project. XUnit automatically creates a test file named UnitTest1.cs.

To replace UnitTest1.cs with the following code example, please refer to the below code example. As an example, we will create a simple test method named TestAddition. The [Fact] attribute indicates that this method is a test.

namespace ZiggyRafiqConsoleApp.Tests
{
    public class ZiggyRafiqConsoleAppTests
    {
        [Fact]
        public void TestAddition()
        {
           // Arrange
           int a = 2;
           int b = 3;

           // Act
           int result = a + b;

           // Assert
           Assert.Equal(5, result);
        }
    }
}

Arrange

Setting up the necessary objects and conditions.

Act

Performing the action or invoking the method under test.

Assert

Verifying that the result is as expected.

Running the Tests

Now, let's run our unit tests.

# Navigate to the test project directory
cd ZiggyRafiqConsoleApp.Tests

# Run the tests
dotnet test
# Navigate to the test project directory
cd ZiggyRafiqConsoleApp.Tests

# Run the tests
dotnet test

There should be an output indicating that one test has been run and passed.

Writing More Tests

Let's add a few more tests to cover different scenarios. Let's replace UnitTest1.cs with the following code example. We will add a subtraction test and a parameterized multiplication test using the [Theory] and [InlineData] attributes.

namespace ZiggyRafiqConsoleApp.Tests
{
    public class ZiggyRafiqConsoleAppTests
    {
        [Fact]
        public void TestSubtraction()
        {
            // Arrange
            int a = 5;
            int b = 3;

            // Act
            int result = a - b;

            // Assert
            Assert.Equal(2, result);
        }

        [Theory]
        [InlineData(2, 3, 6)]
        [InlineData(5, 4, 20)]
        [InlineData(0, 7, 0)]
        public void TestMultiplication(int a, int b, int expected)
        {
            // Act
            int result = a * b;

            // Assert
            Assert.Equal(expected, result);
        }
    }
}

Advanced Concepts


Test Fixture

Generally, a test fixture contains one or more test methods. It can be used to share setup and cleanup code between tests.

namespace ZiggyRafiqConsoleApp
{
    public class CalculatorFixture : IDisposable
    {
        public Calculator Calculator { get; private set; }

        public CalculatorFixture()
        {
            // Initialize resources, create instances, etc.
            Calculator = new Calculator();
        }

        public void Dispose()
        {
            // Clean up resources, dispose of instances, etc.
            Calculator.Dispose();
        }
    }
}
namespace ZiggyRafiqConsoleApp.Tests
{
    public class CalculatorTests : IClassFixture<CalculatorFixture>
    {
        private readonly CalculatorFixture _fixture;

        public CalculatorTests(CalculatorFixture fixture)
        {
            _fixture = fixture;
        }

        [Fact]
        public void TestAddition()
        {
            // Arrange
            Calculator calculator = _fixture.Calculator;
            int a = 5;
            int b = 10;

            // Act
            int result = calculator.Add(a, b);

            // Assert
            Assert.Equal(15, result);
        }
    }
}

Mocking

The use of mocking libraries, such as Moq, can help isolate code units for testing.

using Moq;

namespace ZiggyRafiqConsoleApp.Tests;
public class DataServiceTests
{
    [Fact]
    public void TestDataProcessing()
    {
        // Arrange
        var mockDataSource = new Mock<IDataSource>();
        mockDataSource.Setup(d => d.GetData()).Returns("HELLO, I AM ZIGGY RAFIQ");

        var dataService = new DataService(mockDataSource.Object);

        // Act
        string result = dataService.ProcessData();

        // Assert
        Assert.Equal("HELLO, I AM ZIGGY RAFIQ", result);
    }
}

Summary

A hands-on introduction to unit testing using xUnit in .NET 8 is presented in this guide. We cover writing and running tests, understanding how tests work, and exploring advanced concepts like test fixtures and mocking. Building reliable, maintainable software requires effective unit testing. We need to embrace unit testing as an integral part of our development process as we continue to develop our projects to ensure robustness and correctness.

The code examples I’ve used in this article are at my GitHub Repo dotnet8-xunit-unit-testing-guide and attached to this article as a zip file. I highly recommend you look at them and understand the code examples. Also please do not forget to follow me on my LinkedIn profile https://www.linkedin.com/in/ziggyrafiq/ and on my GitHub https://github.com/ziggyrafiq thank you for your support and reading my articles. I truly appreciate your support thank you.

Happy testing!