iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🌱

How to Implement Unit Testing with C# and xUnit in Visual Studio 2022

に公開

Introduction

When developing applications in C#, you may want to verify whether functions or processes are working correctly.

However, manually checking outputs with Console.WriteLine() every time is time-consuming, and it's easy to overlook bugs.

In this article, I will explain how to easily automate testing on Visual Studio 2022 using the C# unit testing framework xUnit.

  • For those new to testing in C#
  • For those who want to use xUnit but don't know how to use it
  • For those who want to check the operational procedures in Visual Studio as well

This content is intended for such people.

Since I personally struggled with the GUI operations when I first introduced xUnit, I've tried to explain it carefully with many screenshots.

The code used in this explanation is available here:

https://github.com/ryuryu333/csharp_ci_sample

Preparing the App Project

Project Creation

For this demonstration, I created a project using the Console App (.NET Framework) template.

Preparing the Code

We will create a Calculator class and use it in the Program class.

Program
using System;

namespace MyConsoleApp
{
    class Program
    {
        static void Main()
        {
            Calculator myCalculator = new Calculator();
            int result = myCalculator.Add(1, 2);
            Console.WriteLine(result);
        }
    }
}
Calculator
using System;

namespace MyConsoleApp
{
    public class Calculator
    {
        public int Add(int input1, int input2)
        {
            return input1 + input2;
        }
    }
}

Automating Testing with xUnit

How should we verify that the Add() function in the Calculator class we created works correctly?

Changing the arguments of Add() one by one to confirm the results is tedious...

Therefore, we will automate the testing process with xUnit.

https://xunit.net/

https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-dotnet-test

Creating a Test Project

Select File > New > Project.

Creating a new project

Search for "xUnit" and select xUnit Test Project.

Searching for xUnit project

Change the Solution field to Add to solution.

Click Next, select the framework according to the .NET version you are using for development, and click Create.

Project settings

Adding a Reference to the App Project

Configure the settings so that the Calculator class can be referenced from the test project.

Right-click on Solution Explorer > Test Project Name > References.
Select Add Reference....

Adding a project reference 1

Click Projects > Solution at the top left of the screen.
Check the box next to your own app project and click OK to close.

Adding a project reference 2

Creating Test Code

Testing methods in xUnit can be broadly classified into the following two types:

  • Use [Fact] if you just want to see if it works/doesn't work
  • Use [Theory] if the output changes depending on the input
Attribute Main Features
[Fact] Basic test without arguments. Suitable for fixed tests that run under the same conditions every time.
[Theory] Parameterized test with arguments. Allows verifying the same process with multiple inputs.

In [Theory], you specify the input and expected output as a set, such as [InlineData(1, 2, 3)].

Attribute Best Use Case Main Features / Decision Points
[InlineData] When you want to directly write a small amount of simple data Concise and written right where it's visible. Ideal for small datasets.
[MemberData] When you want to reuse slightly complex data or multiple combinations Sufficient if you can statically return arrays, lists, etc.
[ClassData] When complex logic, external data, or dynamic generation is required Allows for flexible processing with a custom class. Best for large or dynamic datasets.

Here is a code example.

CalculatorTest
using Xunit;
using MyConsoleApp;
using System.Collections;
using System.Collections.Generic;

namespace MyTest
{
    public class CalculatorTest
    {
        // Instantiate the class to be tested from MyConsoleApp
        private readonly Calculator _calculator = new();

        [Fact]
        public void AddTestFact()
        {
            Assert.Equal(3, _calculator.Add(1, 2));
        }

        // Pattern using InlineData
        [Theory]
        [InlineData(1, 2, 3)]
        [InlineData(2, 3, 5)]
        public void AddTestTheory(int input1, int input2, int expected)
        {
            int actual = _calculator.Add(input1, input2);
            Assert.Equal(expected, actual);
        }

        // Pattern using MemberData
        [Theory]
        [MemberData(nameof(AddTestMemberData))]
        public void AddTestMember(int a, int b, int expected)
        {
            Assert.Equal(expected, _calculator.Add(a, b));
        }

        public static IEnumerable<object[]> AddTestMemberData =>
            new List<object[]>
            {
                    new object[] { 1, 2, 3 },
                    new object[] { 5, 7, 12 }
            };

        // Pattern using ClassData
        [Theory]
        [ClassData(typeof(AddTestData))]
        public void AddTestClassData(int a, int b, int expected)
        {
            int result = _calculator.Add(a, b);
            Assert.Equal(expected, result);
        }
    }

    // Test data class for ClassData
    public class AddTestData : IEnumerable<object[]>
    {
        public IEnumerator<object[]> GetEnumerator()
        {
            yield return new object[] { 10, 20, 30 };
            yield return new object[] { -5, 5, 0 };
            yield return new object[] { 100, 200, 300 };
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

Running Tests

Click Test > Test Explorer.

How to open Test Explorer

Clicking the arrow button at the top left of the Test Explorer screen will execute all tests.

Running Tests

If a test fails, it will be marked with a red X.

By selecting the corresponding row, you can check the actual values. In this case, you can see that while the expected output was 4, the actual output was 3.

When a test fails

Useful Tips

Specifying Test Names

Test results display the function names by default.

By using DisplayName, you can display any name you like.

Specifying a test name

CalculatorTest
using Xunit;
using MyConsoleApp;
using System.Collections;
using System.Collections.Generic;

namespace MyTest
{
    public class CalculatorTest
    {
        // Instantiate the class to be tested from MyConsoleApp
        private readonly Calculator _calculator = new();

        [Fact(DisplayName = "You can display any name you like")]
        public void AddTestDisplayName()
        {
            Assert.Equal(3, _calculator.Add(1, 2));
        }
    }
}

Outputting Logs

If you want to check logs during test execution, Console.WriteLine will not display them correctly.

By using ITestOutputHelper, you will be able to check the logs.

Outputting logs

CalculatorTest
using Xunit;
using MyConsoleApp;
using System.Collections;
using System.Collections.Generic;

namespace MyTest
{
    public class CalculatorTest
    {
        // Instantiate the class to be tested from MyConsoleApp
        private readonly Calculator _calculator = new();

        private readonly ITestOutputHelper _output;

        public CalculatorTest(ITestOutputHelper output)
        {
            _output = output;
        }

        [Fact]
        public void AddTestLog()
        {
            Assert.Equal(3, _calculator.Add(1, 2));
            _output.WriteLine("You can display logs");
            Console.WriteLine("This one will not be displayed");
        }
    }
}

Conclusion

So far, we have introduced the steps for implementing unit tests for C# functions using xUnit, along with the screen operations in Visual Studio 2022.

By automating tests, you can establish an environment for early bug detection and refactoring with peace of mind.

The next step is to introduce CI (Continuous Integration).

By setting up a system where tests are automatically executed at the time of a Push or Pull Request, even more efficient development becomes possible.

https://zenn.dev/trifolium/articles/6f6fe5c8746798

Discussion