Unit Testing in .NET with xUnit

A Beginner’s Guide to Unit Testing C# code in .NET

Roko Kovač
5 min readApr 23, 2023

Introduction

In this article, I will cover the basics and set you up for a road to simpler, cleaner and more maintainable code in .NET 7 using xUnit.

Get the full Quickstart Guide on Unit Testing in .NET here.

The Benefits of Unit Testing

Unit Tests are small, fast, automated tests that test specific behaviors in isolation.

Unit Tests, among other things:

  • provide short feedback loops,
  • improve code quality and maintainability,
  • reduce bug density.

The Testing Framework — xUnit

As we’re only covering the basics, the only thing we need is a testing framework to run our tests and write basic assertions.

The most popular testing framework in the .NET ecosystem is xUnit. It’s free, open source, has a long history and a big community. This is what we will be using.

Example Project

Consider the following example of a simplified ASP.NET Core 7 app :

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<CarService>();
var app = builder.Build();
app.MapGet("/cars/{make}/models", (string make, CarService cs) => cs.GetModels(make));
app.Run();

public class CarService
{
public string[] GetModels(string make) => make switch
{
"Honda" => new[] { "Civic", "Accord", "CR-V" },
"Tesla" => new[] { "Model S", "Model Y" },
"Ford" => new[] { "Mustang" },
_ => throw new ArgumentException("Invalid car make", nameof(make))
};
}

The Test Project

Let’s create the test project!

First, make sure you’ve installed the .NET 7 SDK.

Then, starting in your solution folder, execute the following commands:

dotnet new xunit -o YourProject.Tests 
dotnet sln add YourProject.Tests

This will create a new project with the xUnit template and add it to your solution.

You will see that the following class has been generated:

public class UnitTest1
{
[Fact]
public void Test1()
{

}
}

Everything should look familiar, except for the Fact attribute.

Facts

Every test method in xUnit is decorated with either a Fact or a Theory attribute, which are used by xUnit to discover tests in our solution.

Theories are beyond the scope of this guide. For now, it’s enough to mention that they’re used to write parameterized tests.

In this guide, we will be focusing on simple Facts.

Defining Test Cases

The behaviors we test are often referred to as test cases. When defining test cases, we follow a simple pattern:

  • Given these conditions
  • When this action is performed
  • Then this result will occur

The Given/When/Then pattern is more often referred to as AAA:

  • Arrange: setting up initial state
  • Act: invoking the behavior we’re testing
  • Assert: validating the result

I prefer calling it the Given/When/Then pattern as it’s more natural, but they’re essentially the same thing.

Our First Test

Let’s write our first test!

Say that we want to ensure that when the GetModels function is called for the make “Ford”, the result is “Mustang”.

Remembering the simple Given/When/Then pattern we mentioned previously, we can write the following test case in plain english:

// Given a CarService

// When GetModels is called with the parameter "Ford"

// Then we should get a list containing one item - "Mustang"

Let’s turn this into code.

public class CarServiceTests
{
[Fact]
public void GetModels_Ford_ReturnsMustang()
{
var carService = new CarService();

var models = carService.GetModels("Ford");

Assert.Single(models);
Assert.Single(models, "Mustang");
}
}

Our code contains two assertions. The first one asserts that the models array contains only a single item, and the second one asserts that that item is “Mustang”.

Test Naming Conventions

At first glance, you will notice the following things:

  • we have renamed the test class to CarServiceTests.
  • We have given the function a descriptive name in the Method_Scenario_ExpectedBehavior pattern.
  • We have visually separated our Given/When/Then code

These three best practices make the tests more descriptive, readable and maintainable. It is very important to keep your tests clean, as they act as a sort of documentation of the business rules.

There are many other test method naming conventions. This one is recommended by Microsoft, and widely adopted, but you can use whichever works for you and your team, as long as you stay consistent.

Running Tests

We can run our test using the command dotnet test

Starting test execution, please wait...
A total of 1 test files matched the specified pattern.

Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: < 1 ms - UnitTestingIntro.Tests.dll (net6.0)

Alternatively, if you’re not a savage, you can use your IDE’s test explorer. I use Rider, but Visual Studio, as well as other popular IDEs and code editors should have one.

Unit Tests window in Rider 2023.1
Test Explorer in Visual Studio 2022

As we can see, the test passes.

Detecting Regressions

The main purpose of automated testing is to make sure we don’t introduce any regressions when developing new features.

Let’s imagine, in the future, you decide to expand the GetModels method. You’re very tired, and make a silly typo:

public string[] GetModels(string make) => make switch
{
"Honda" => new[] { "Civic", "Accord", "CR-V" },
"Tesla" => new[] { "Model S", "Model Y" },
"Ford" => new[] { "Mustnag" },
"Skoda" => new[] { "Fabia", "Octavia" },
_ => throw new ArgumentException("Invalid car make", nameof(make))
};

After running the tests again, you get a nice, descriptive message telling you exactly what you broke:

The collection was expected to contain a single element matching "Mustang", 
but it contained no matching elements.

This might seem pointless, as it’s an obvious mistake made for demonstration purposes, but after employing unit testing for some time you will be surprised at how many silly mistakes are caught before they can do any damage and how easier and faster it is to build new features.

You can now fix the regression you made and, being a hard working, dilligent developer, ensure that the new behavior is tested as well.

Conclusion

I have shown you how to set up your project for testing and write your first test.

This is only the beginning, as there’s much more to learn and practice.

In my future blog posts, I will get into more advanced techniques and strategies for Unit Testing like parameterizing tests, mocking dependencies, Test Driven Development, Integration Testing and tools to make your tests cleaner and easier to write.

If you’re interested in those topics, subscribe to get notified when I post more.

--

--