Getting Started with xUnit.net

Using .NET Framework with the .NET SDK command line

In this article, we will demonstrate getting started with xUnit.net and .NET Framework, showing you how to write and run your first set of unit tests.

Note: The examples were done with xUnit.net v2.4.1, .NET SDK 5.0.102, and .NET Framework 4.8. The version numbers, paths, and generated templates may differ for you, depending on which version you're using. Note that .NET Framework 4.5.2 and later is supported.

Download the .NET SDK

Although the .NET SDK was designed originally to support .NET Core, it can also be used to allow cross-platform .NET Framework development. We will use it to build our projects. You may also opt to use the command line tools from Visual Studio, though that is outside the scope of this document.

As of this writing, the .NET SDK is available for download for Windows, Linux, and macOS. Once you've downloaded and installed the SDK, open a fresh command prompt of your choice (CMD, PowerShell, Bash, etc.) and make sure that you can access the CLI by typing dotnet --version. You should be rewarded with a single line, describing the version of the .NET Core SDK you have installed:

$ dotnet --version
5.0.102

Note: the first time you run the dotnet command, it may perform some post-installation steps. Once these one-time actions are done, it will execute your command.

Download Mono (non-Windows machines)

While you can use the .NET SDK to build .NET Framework apps on non-Windows machines, you still need a way to run them. That means Mono. If you are using a non-Windows machine, download and install Mono on your machine.

Create the unit test project

From the command line, create a folder for your test project, change into it, and then create an empty solution file using dotnet new:

$ mkdir MyFirstUnitTests
$ cd MyFirstUnitTests
$ dotnet new sln
The template "Solution File" was created successfully.

Now we will create a unit test project, again with dotnet new, and add it to our solution:

$ dotnet new xunit
The template "xUnit Test Project" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on ~/dev/MyFirstUnitTests/MyFirstUnitTests.csproj...
  Determining projects to restore...
  Restored ~/dev/MyFirstUnitTests/MyFirstUnitTests.csproj (in 217 ms).
Restore succeeded.

$ dotnet sln add .
Project `MyFirstUnitTests.csproj` added to the solution.

Because the .NET SDK command line tool is optimized around supporting .NET Core, it created a unit test project that targets the incorrect framework. Open up your text editor and change the contents of the .csproj file to match the contents here:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference
        Include="Microsoft.NETFramework.ReferenceAssemblies"
        Version="1.0.0"
        Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.console" Version="2.4.1">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

</Project>

Let's quickly review what's in this project file:

A single empty unit test was also generated into UnitTest1.cs:

using System;
using Xunit;

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

        }
    }
}

Let's build everything and take a look at our output folder:

$ dotnet build
Microsoft (R) Build Engine version 16.8.3+39993bd9d for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored ~/dev/MyFirstUnitTests/MyFirstUnitTests.csproj (in 93 ms).
  MyFirstUnitTests -> ~/dev/MyFirstUnitTests/bin/Debug/net48/MyFirstUnitTests.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:00.82

$ dir bin/Debug/net48
MyFirstUnitTests.dll
MyFirstUnitTests.pdb
xunit.abstractions.dll
xunit.assert.dll
xunit.core.dll
xunit.execution.desktop.dll

We see our binary there, along with the xunit.* dependency DLLs. What we don't see, though, is the console runner. That's because the console runner reference doesn't bring any libraries; it only brings binaries.

We can see those binaries by peeking into our NuGet package cache:

$ find ~/.nuget/packages/xunit.runner.console/2.4.1 -name "*.exe"
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net452/xunit.console.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net452/xunit.console.x86.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net46/xunit.console.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net46/xunit.console.x86.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net461/xunit.console.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net461/xunit.console.x86.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net462/xunit.console.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net462/xunit.console.x86.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net47/xunit.console.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net47/xunit.console.x86.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net471/xunit.console.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net471/xunit.console.x86.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net472/xunit.console.exe
~/.nuget/packages/xunit.runner.console/2.4.1/tools/net472/xunit.console.x86.exe

At the time we shipped xUnit.net 2.4.1, we provided versions of xunit.console.exe compiled against every supported version of .NET Framework that existed at the time (the only one that's missing is .NET 4.8, since it's newer than xUnit.net 2.4.1, though of course the version that's compiled against .NET 4.7.2 will work just fine for running 4.8 tests).

You'll also note that we ship versions that are compiled to run as 32-bit on 64-bit OSes, named xunit.console.x86.exe. This feature, though, only works on Windows. When you install .NET Framework on Windows, it installs both a 32-bit and 64-bit version that live side-by-side, and the launcher on Windows will detect whether your program is compiled for 32-bit, 64-bit, or agnostic, and use the appropriate version of .NET Framework. When running on non-Windows machines, you'll be running using Mono, and that only ships a 64-bit versions for Linux and macOS.

Now let's verify that everything is working. Windows user can run the console runner directly, whereas Linux and macOS users may need to invoke the console runner using the mono application.

Windows

$ ~/.nuget/packages/xunit.runner.console/2.4.1/tools/net472/xunit.console.exe bin/Debug/net48/MyFirstUnitTests.dll
xUnit.net Console Runner v2.4.1 (64-bit Desktop .NET 4.7.2, runtime: 4.0.30319.42000)
  Discovering: MyFirstUnitTests
  Discovered:  MyFirstUnitTests
  Starting:    MyFirstUnitTests
  Finished:    MyFirstUnitTests
=== TEST EXECUTION SUMMARY ===
   MyFirstUnitTests  Total: 1, Errors: 0, Failed: 0, Skipped: 0, Time: 0.082s

Non-Windows

$ mono ~/.nuget/packages/xunit.runner.console/2.4.1/tools/net472/xunit.console.exe bin/Debug/net48/MyFirstUnitTests.dll
xUnit.net Console Runner v2.4.1 (64-bit Desktop .NET 4.7.2, runtime: 4.0.30319.42000)
  Discovering: MyFirstUnitTests
  Discovered:  MyFirstUnitTests
  Starting:    MyFirstUnitTests
  Finished:    MyFirstUnitTests
=== TEST EXECUTION SUMMARY ===
   MyFirstUnitTests  Total: 1, Errors: 0, Failed: 0, Skipped: 0, Time: 0.082s

Excellent! Everything is running.

Setting up a unit test MSBuild target

You may have noticed that it's kind of cumbersome running tests this way from the command line. Luckily, we can take advantage of extensibility in MSBuild to do this for us (and, conveniently, the .csproj file is an MSBuild project file).

If you dig around inside the ~/.nuget/packages/xunit.console/2.4.1/build folder, you'll notice that there is a .props file there. Among other things, it pre-defines some MSBuild properties that we can use when writing our own custom tasks:

[...]
  <!-- Version specific -->
  <XunitConsole452Path>$(MSBuildThisFileDirectory)../tools/net452/xunit.console.exe</XunitConsole452Path>
  <XunitConsole452PathX86>$(MSBuildThisFileDirectory)../tools/net452/xunit.console.x86.exe</XunitConsole452PathX86>
  <XunitConsole46Path>$(MSBuildThisFileDirectory)../tools/net46/xunit.console.exe</XunitConsole46Path>
  <XunitConsole46PathX86>$(MSBuildThisFileDirectory)../tools/net46/xunit.console.x86.exe</XunitConsole46PathX86>
  <XunitConsole461Path>$(MSBuildThisFileDirectory)../tools/net461/xunit.console.exe</XunitConsole461Path>
  <XunitConsole461PathX86>$(MSBuildThisFileDirectory)../tools/net461/xunit.console.x86.exe</XunitConsole461PathX86>
  <XunitConsole462Path>$(MSBuildThisFileDirectory)../tools/net462/xunit.console.exe</XunitConsole462Path>
  <XunitConsole462PathX86>$(MSBuildThisFileDirectory)../tools/net462/xunit.console.x86.exe</XunitConsole462PathX86>
  <XunitConsole47Path>$(MSBuildThisFileDirectory)../tools/net47/xunit.console.exe</XunitConsole47Path>
  <XunitConsole47PathX86>$(MSBuildThisFileDirectory)../tools/net47/xunit.console.x86.exe</XunitConsole47PathX86>
  <XunitConsole471Path>$(MSBuildThisFileDirectory)../tools/net471/xunit.console.exe</XunitConsole471Path>
  <XunitConsole471PathX86>$(MSBuildThisFileDirectory)../tools/net471/xunit.console.x86.exe</XunitConsole471PathX86>
  <XunitConsole472Path>$(MSBuildThisFileDirectory)../tools/net472/xunit.console.exe</XunitConsole472Path>
  <XunitConsole472PathX86>$(MSBuildThisFileDirectory)../tools/net472/xunit.console.x86.exe</XunitConsole472PathX86>
[...]

First, update the PropertyGroup to include a new conditional value. We'll define an XunitConsole property which will point to our chosen console runner, and when running on non-Windows OSes, will make sure we run it using Mono:

<XunitConsole Condition=" '$(OS)' == 'Windows_NT' ">&quot;$(XunitConsole472Path)&quot;</XunitConsole>
<XunitConsole Condition=" '$(OS)' != 'Windows_NT' ">mono &quot;$(XunitConsole472Path)&quot;</XunitConsole>

Second, let's add a new Target to our project file:

<Target Name="Test" DependsOnTargets="Build">
  <Exec
      Command="$(XunitConsole) $(TargetFileName)"
      WorkingDirectory="$(TargetDir)" />
</Target>

This is what our total .csproj file should look like now:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
    <XunitConsole Condition=" '$(OS)' == 'Windows_NT' ">&quot;$(XunitConsole472Path)&quot;</XunitConsole>
    <XunitConsole Condition=" '$(OS)' != 'Windows_NT' ">mono &quot;$(XunitConsole472Path)&quot;</XunitConsole>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference
        Include="Microsoft.NETFramework.ReferenceAssemblies"
        Version="1.0.0"
        Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.console" Version="2.4.1">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
  </ItemGroup>

  <Target Name="Test" DependsOnTargets="Build">
    <Exec
        Command="$(XunitConsole) $(TargetFileName)"
        WorkingDirectory="$(TargetDir)" />
  </Target>

</Project>

Now we can use dotnet build to run our tests:

$ dotnet build -t:Test
Microsoft (R) Build Engine version 16.8.3+39993bd9d for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  MyFirstUnitTests -> /home/bradwilson/dev/MyFirstUnitTests/bin/Debug/net48/MyFirstUnitTests.dll
  xUnit.net Console Runner v2.4.1 (64-bit Desktop .NET 4.7.2, runtime: 4.0.30319.42000)
    Discovering: MyFirstUnitTests
    Discovered:  MyFirstUnitTests
    Starting:    MyFirstUnitTests
    Finished:    MyFirstUnitTests
  === TEST EXECUTION SUMMARY ===
     MyFirstUnitTests  Total: 1, Errors: 0, Failed: 0, Skipped: 0, Time: 0.063s

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.49

This runs our Test target, which will first build our project (since the target included DependsOnTargets="Build").

Let's go replace that empty unit test with our first real tests.

Write your first tests

Using your favorite text editor, open the UnitTest1.cs file and add a couple tests:

using Xunit;

namespace MyFirstUnitTests
{
    public class UnitTest1
    {
        [Fact]
        public void PassingTest()
        {
            Assert.Equal(4, Add(2, 2));
        }

        [Fact]
        public void FailingTest()
        {
            Assert.Equal(5, Add(2, 2));
        }

        int Add(int x, int y)
        {
            return x + y;
        }
    }
}

Now let's go run the tests again and see what happens:

$ dotnet build -t:Test
Microsoft (R) Build Engine version 16.8.3+39993bd9d for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  MyFirstUnitTests -> ~/dev/MyFirstUnitTests/bin/Debug/net48/MyFirstUnitTests.dll
  xUnit.net Console Runner v2.4.1 (64-bit Desktop .NET 4.7.2, runtime: 4.0.30319.42000)
    Discovering: MyFirstUnitTests
    Discovered:  MyFirstUnitTests
    Starting:    MyFirstUnitTests
      MyFirstUnitTests.UnitTest1.FailingTest [FAIL]
        Assert.Equal() Failure
        Expected: 5
        Actual:   4
        Stack Trace:
          ~/dev/MyFirstUnitTests/UnitTest1.cs(16,0): at MyFirstUnitTests.UnitTest1.FailingTest()
    Finished:    MyFirstUnitTests
  === TEST EXECUTION SUMMARY ===
     MyFirstUnitTests  Total: 2, Errors: 0, Failed: 1, Skipped: 0, Time: 0.150s
MyFirstUnitTests.csproj(25,5): error MSB3073: The command "xunit.console.exe MyFirstUnitTests.dll" exited with code 1.

Build FAILED.

MyFirstUnitTests.csproj(25,5): error MSB3073: The command "xunit.console.exe MyFirstUnitTests.dll" exited with code 1.
    0 Warning(s)
    1 Error(s)

Time Elapsed 00:00:01.60

Now that we've gotten your first unit tests to run, let's introduce one more way to write tests: using theories.

Write your first theory

You may have wondered why your first unit tests use an attribute named [Fact] rather than one with a more traditional name like Test. xUnit.net includes support for two different major types of unit tests: facts and theories. When describing the difference between facts and theories, we like to say:

Facts are tests which are always true. They test invariant conditions.

Theories are tests which are only true for a particular set of data.

A good example of this is testing numeric algorithms. Let's say you want to test an algorithm which determines whether a number is odd or not. If you're writing the positive-side tests (odd numbers), then feeding even numbers into the test would cause it fail, and not because the test or algorithm is wrong.

Let's add a theory to our existing facts (including a bit of bad data, so we can see it fail):

[Theory]
[InlineData(3)]
[InlineData(5)]
[InlineData(6)]
public void MyFirstTheory(int value)
{
    Assert.True(IsOdd(value));
}

bool IsOdd(int value)
{
    return value % 2 == 1;
}

This time when we run our tests, we see a second failure, for our theory that was given 6:

  Determining projects to restore...
  All projects are up-to-date for restore.
  MyFirstUnitTests -> ~/dev/MyFirstUnitTests/bin/Debug/net48/MyFirstUnitTests.dll
  xUnit.net Console Runner v2.4.1 (64-bit Desktop .NET 4.7.2, runtime: 4.0.30319.42000)
    Discovering: MyFirstUnitTests
    Discovered:  MyFirstUnitTests
    Starting:    MyFirstUnitTests
      MyFirstUnitTests.UnitTest1.FailingTest [FAIL]
        Assert.Equal() Failure
        Expected: 5
        Actual:   4
        Stack Trace:
          ~/dev/MyFirstUnitTests/UnitTest1.cs(16,0): at MyFirstUnitTests.UnitTest1.FailingTest()
      MyFirstUnitTests.UnitTest1.MyFirstTheory(value: 6) [FAIL]
        Assert.True() Failure
        Expected: True
        Actual:   False
        Stack Trace:
          ~/dev/MyFirstUnitTests/UnitTest1.cs(30,0): at MyFirstUnitTests.UnitTest1.MyFirstTheory(Int32 value)
    Finished:    MyFirstUnitTests
  === TEST EXECUTION SUMMARY ===
     MyFirstUnitTests  Total: 5, Errors: 0, Failed: 2, Skipped: 0, Time: 0.171s
MyFirstUnitTests.csproj(25,5): error MSB3073: The command "xunit.console.exe MyFirstUnitTests.dll" exited with code 1.

Build FAILED.

MyFirstUnitTests.csproj(25,5): error MSB3073: The command "xunit.console.exe MyFirstUnitTests.dll" exited with code 1.
    0 Warning(s)
    1 Error(s)

Time Elapsed 00:00:01.14

Although we've only written 3 test methods, the test runner actually ran 5 tests; that's because each theory with its data set is a separate test. Note also that the runner tells you exactly which set of data failed, because it includes the parameter values in the name of the test.

Running tests with Visual Studio

If you're having problems discovering or running tests, you may be a victim of a corrupted runner cache inside Visual Studio. To clear this cache, shut down all instances of Visual Studio, then delete the folder %TEMP%/VisualStudioTestExplorerExtensions. Also make sure your solution is only linked against a single version of the Visual Studio runner NuGet package (xunit.runner.visualstudio).

If you have Visual Studio Community (or a paid-for version of Visual Studio), you can run your xUnit.net tests within Visual Studio's built-in test runner (named Test Explorer). Unfortunately, this does not include Express editions of Visual Studio (you should upgrade to the free Community Edition instead).

Edit your .csproj file and add two package references (xunit.runner.visualstudio and Microsoft.NET.Test.Sdk). Your updated ItemGroup should look like this:

<ItemGroup>
  <PackageReference
      Include="Microsoft.NETFramework.ReferenceAssemblies"
      Version="1.0.0"
      Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
  <PackageReference Include="xunit" Version="2.4.1" />
  <PackageReference Include="xunit.runner.console" Version="2.4.1" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
  </PackageReference>
</ItemGroup>

Now open your solution with Visual Studio. (The screen shots and menu items here are taken from Visual Studio 2019; your version may be slightly different.)

Make sure Test Explorer is visible (go to Test > Test Explorer). Depending on the version of Visual Studio you have, you may need to build your test assembly before tests are discovered. After a moment of discovery, you should see the list of discovered tests:

Click the Run All link in the Test Explorer window, and you should see the results update in the Test Explorer window as the tests are run:

You can click on a failed test to see the failure message, and the stack trace. You can click on the stack trace lines to take you directly to the failing line of code.

Copyright © .NET Foundation. Contributions welcomed at https://github.com/xunit/xunit/tree/gh-pages.