ZetCode

C# NUnit

last modified October 17, 2026

In this article we show how to do unit testing in C# with the NUnit framework.

Unit testing is a software testing method where individual units (components) of software are tested. The purpose of unit testing is to validate that each unit of the software performs as designed. A unit is the smallest testable part of any software, typically a method or a class.

NUnit is a unit-testing framework for all .NET languages. It was originally ported from Java's JUnit. Other popular unit-testing frameworks are xUnit and MSTest. The examples were tested with NUnit 4.3 and .NET 10.

The tests are placed in a separate test project, which references the project being tested. Both projects belong to one solution.

Creating the projects

We create a solution with a class library and a test project.

$ mkdir Services
$ cd Services
$ dotnet new sln
$ dotnet new classlib -o Services
$ dotnet new nunit -o Services.Tests
$ dotnet sln add Services/Services.csproj Services.Tests/Services.Tests.csproj
$ dotnet add Services.Tests reference Services/Services.csproj

The dotnet new sln command creates a solution file; since .NET 10, it uses the XML-based .slnx format. The nunit template creates a test project with all the necessary packages. We add both projects to the solution and reference the library from the test project. We delete the generated Class1.cs and UnitTest1.cs files.

Services.Tests/Services.Tests.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.4" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
    <PackageReference Include="NUnit" Version="4.3.2" />
    <PackageReference Include="NUnit.Analyzers" Version="4.7.0" />
    <PackageReference Include="NUnit3TestAdapter" Version="5.0.0" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="NUnit.Framework" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Services\Services.csproj" />
  </ItemGroup>

</Project>

This is the project file of the test project. The NUnit package is the framework itself. The NUnit3TestAdapter and Microsoft.NET.Test.Sdk packages allow dotnet test to discover and run the tests; despite its name, the adapter works with NUnit 4. The NUnit.Analyzers package contains code analyzers that warn about the incorrect use of NUnit, and coverlet.collector collects code coverage. The Using item imports the NUnit.Framework namespace in all files of the project.

C# NUnit simple example

We start with a simple example. We test the following arithmetic functions.

Services/Basic.cs
namespace Services;

public static class Basic
{
    public static int Add(int a, int b) => a + b;
    public static int Sub(int a, int b) => a - b;
    public static int Mul(int a, int b) => a * b;
    public static int Div(int a, int b) => a / b;
}

The Basic class is part of the Services library.

Services.Tests/BasicTests.cs
namespace Services.Tests;

public class BasicTests
{
    [Test]
    public void AddTwoNumbers()
    {
        Assert.That(Basic.Add(2, 3), Is.EqualTo(5));
    }

    [Test]
    public void SubTwoNumbers()
    {
        Assert.That(Basic.Sub(2, 3), Is.EqualTo(-1));
    }
}

The test methods are annotated with the [Test] attribute. NUnit discovers them automatically; the class does not need any attribute.

Assert.That(Basic.Add(2, 3), Is.EqualTo(5));

We use assertions to ensure the correct output. NUnit uses the constraint model of assertions: the Assert.That method takes the actual value and a constraint, such as Is.EqualTo(5), which the value must satisfy.

Older NUnit code uses the classic assertions, such as Assert.AreEqual(5, r). Since NUnit 4, they are moved to the ClassicAssert class in the NUnit.Framework.Legacy namespace. New code should use Assert.That.

$ dotnet test
...
A total of 1 test files matched the specified pattern.

Passed!  - Failed:     0, Passed:     2, Skipped:     0, Total:     2, Duration: 21 ms - Services.Tests.dll (net10.0)

The dotnet test command builds the solution and runs all tests.

C# NUnit parameterized tests

A test method can take parameters. The values are provided with the [TestCase] attribute; the method is executed once for each test case.

Services.Tests/ParamTests.cs
namespace Services.Tests;

public class ParamTests
{
    [TestCase(1, 2, 3)]
    [TestCase(2, 2, 4)]
    [TestCase(-1, 4, 3)]
    public void Add(int x, int y, int expected)
    {
        Assert.That(Basic.Add(x, y), Is.EqualTo(expected));
    }

    [TestCase(1, 2, ExpectedResult = -1)]
    [TestCase(2, 2, ExpectedResult = 0)]
    [TestCase(3, 2, ExpectedResult = 1)]
    public int Sub(int x, int y)
    {
        return Basic.Sub(x, y);
    }

    [TestCase(9, 3, 27)]
    [TestCase(-3, -3, 9)]
    [Ignore("Not ready yet")]
    public void Mul(int x, int y, int expected)
    {
        Assert.That(Basic.Mul(x, y), Is.EqualTo(expected));
    }

    [TestCaseSource(nameof(DivCases))]
    public void Div(int x, int y, int expected)
    {
        Assert.That(Basic.Div(x, y), Is.EqualTo(expected));
    }

    private static readonly object[] DivCases =
    [
        new object[] { 9, 3, 3 },
        new object[] { 3, 3, 1 },
        new object[] { 8, 3, 2 }
    ];

    [Test]
    public void DivByZero()
    {
        Assert.That(() => Basic.Div(5, 0), Throws.TypeOf<DivideByZeroException>());
    }
}

In this example, we test the arithmetic methods with several sets of values.

[TestCase(1, 2, 3)]
[TestCase(2, 2, 4)]
[TestCase(-1, 4, 3)]
public void Add(int x, int y, int expected)

The Add method is tested with three sets of values given by the [TestCase] attributes.

[TestCase(1, 2, ExpectedResult = -1)]
[TestCase(2, 2, ExpectedResult = 0)]
[TestCase(3, 2, ExpectedResult = 1)]
public int Sub(int x, int y)
{
    return Basic.Sub(x, y);
}

With the ExpectedResult property, the test method returns the actual value and NUnit compares it with the expected result. We do not need to write the assertion.

[Ignore("Not ready yet")]

Test methods can be skipped with the [Ignore] attribute. The reason is mandatory and is shown in the test output.

[TestCaseSource(nameof(DivCases))]
public void Div(int x, int y, int expected)
...

private static readonly object[] DivCases =
[
    new object[] { 9, 3, 3 },
    new object[] { 3, 3, 1 },
    new object[] { 8, 3, 2 }
];

The [TestCaseSource] attribute reads the test data from a static field, property, or method. Each item of the array is an array of arguments for one test case. This is useful when the data cannot be written as attribute arguments, or when it is shared by several tests.

Assert.That(() => Basic.Div(5, 0), Throws.TypeOf<DivideByZeroException>());

To test exceptions, we pass a lambda to Assert.That together with a Throws constraint. The Throws.TypeOf requires the exact exception type; Throws.InstanceOf also accepts derived types.

$ dotnet test
...
  Skipped Mul(9,3,27) [8 ms]
  Skipped Mul(-3,-3,9) [8 ms]

Passed!  - Failed:     0, Passed:    12, Skipped:     2, Total:    14, Duration: 34 ms - Services.Tests.dll (net10.0)

Each test case is counted as a separate test.

C# NUnit SetUp

The method annotated with the [SetUp] attribute runs before each test method. It is used to create a fresh state for every test. The [TearDown] method runs after each test. The [OneTimeSetUp] and [OneTimeTearDown] methods run only once for all tests of the class.

Services/PalindromeService.cs
using System.Globalization;

namespace Services;

public class PalindromeService
{
    public bool IsPalindrome(string word)
    {
        ArgumentException.ThrowIfNullOrEmpty(word);

        string[] elements = GetTextElements(word.ToLowerInvariant());
        string[] reversed = elements.Reverse().ToArray();

        return elements.SequenceEqual(reversed);
    }

    private static string[] GetTextElements(string s)
    {
        var elements = new List<string>();
        TextElementEnumerator e = StringInfo.GetTextElementEnumerator(s);

        while (e.MoveNext())
        {
            elements.Add(e.GetTextElement());
        }

        return [.. elements];
    }
}

The IsPalindrome method determines if a word is a palindrome. It ignores the case of the letters. It works with text elements (grapheme clusters) rather than individual char values, so that letters composed of several code points are not broken when the word is reversed. For an empty or null word, it throws an exception.

Services.Tests/PalindromeTests.cs
namespace Services.Tests;

public class PalindromeTests
{
    private PalindromeService service = null!;

    [SetUp]
    public void SetUp()
    {
        service = new PalindromeService();
    }

    [TestCase("racecar")]
    [TestCase("Level")]
    [TestCase("ťoť")]
    public void IsPalindrome(string word)
    {
        Assert.That(service.IsPalindrome(word), Is.True);
    }

    [TestCase("falcon")]
    [TestCase("forest")]
    public void IsNotPalindrome(string word)
    {
        Assert.That(service.IsPalindrome(word), Is.False);
    }

    [Test]
    public void EmptyWord()
    {
        Assert.Throws<ArgumentException>(() => service.IsPalindrome(""));
    }
}

We test the IsPalindrome method with palindromes, ordinary words, and an empty string.

private PalindromeService service = null!;

[SetUp]
public void SetUp()
{
    service = new PalindromeService();
}

A new PalindromeService is created before each test. NUnit creates only one instance of the test class for all its tests, so the [SetUp] method is the place to reset the state. The null! tells the compiler that the field is initialized before it is used.

Assert.Throws<ArgumentException>(() => service.IsPalindrome(""));

The Assert.Throws method is an alternative way of testing exceptions. It requires the exact exception type and returns the exception, so that we can inspect it further.

C# NUnit constraints

NUnit has a rich set of constraints. They are available through the Is, Has, Does, and Throws classes, and they can be combined with modifiers.

Services.Tests/ConstraintTests.cs
namespace Services.Tests;

public class ConstraintTests
{
    [Test]
    public void Constraints()
    {
        int[] vals = [1, 2, 3, 4, 5];
        string word = "falcon";

        using (Assert.EnterMultipleScope())
        {
            Assert.That(vals, Has.Length.EqualTo(5));
            Assert.That(vals, Does.Contain(3));
            Assert.That(vals, Is.Ordered);
            Assert.That(vals, Is.All.GreaterThan(0));

            Assert.That(word, Does.StartWith("fal"));
            Assert.That(word, Is.EqualTo("FALCON").IgnoreCase);

            Assert.That(vals.Average() / 10, Is.EqualTo(0.3).Within(1e-9));
            Assert.That(Basic.Add(2, 2), Is.InRange(3, 5));
        }
    }
}

We check an array, a string, and numbers with various constraints.

using (Assert.EnterMultipleScope())
{
    ...
}

Normally, a test stops at the first failed assertion. Inside a multiple scope, all assertions are evaluated and all failures are reported together at the end of the block. The EnterMultipleScope method was added in NUnit 4.2; older code uses Assert.Multiple with a lambda.

Assert.That(vals, Is.Ordered);
Assert.That(vals, Is.All.GreaterThan(0));

Collection constraints check the order of the elements or apply a constraint to every element.

Assert.That(word, Is.EqualTo("FALCON").IgnoreCase);

The IgnoreCase modifier makes the comparison case-insensitive.

Assert.That(vals.Average() / 10, Is.EqualTo(0.3).Within(1e-9));

Floating-point values should be compared with a tolerance, which is set with the Within modifier.

$ dotnet test
...
Passed!  - Failed:     0, Passed:    19, Skipped:     2, Total:    21, Duration: 37 ms - Services.Tests.dll (net10.0)

C# NUnit failed test

Finally, we look at how a failed test is reported. Suppose we have the following test with two wrong assertions.

Services.Tests/FailTests.cs
namespace Services.Tests;

public class FailTests
{
    [Test]
    public void Failing()
    {
        int[] vals = [1, 2, 3];

        using (Assert.EnterMultipleScope())
        {
            Assert.That(Basic.Mul(3, 3), Is.EqualTo(6));
            Assert.That(vals, Does.Contain(4));
        }
    }
}

Both assertions fail. Because they are in a multiple scope, both failures are reported.

$ dotnet test
...
  Failed Failing [21 ms]
  Error Message:
   Multiple failures or warnings in test:
  1)   Assert.That(Basic.Mul(3, 3), Is.EqualTo(6))
  Expected: 6
  But was:  9

  2)   Assert.That(vals, Does.Contain(4))
  Expected: some item equal to 4
  But was:  < 1, 2, 3 >
...
Failed!  - Failed:     1, Passed:    19, Skipped:     2, Total:    22, Duration: 49 ms - Services.Tests.dll (net10.0)

The error message shows the failed expressions together with the expected and the actual values.

Source

Unit testing C# with NUnit and .NET

NUnit documentation

In this article we have done unit testing in C# with the NUnit framework.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.