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.
<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.
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.
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.
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.
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.
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.
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.
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
In this article we have done unit testing in C# with the NUnit framework.
Author
List all C# tutorials.