Writing Unit Tests in C# (xUnit, NUnit, MSTest)
Learn how to write unit tests in C# using xUnit, NUnit, and MSTest to build reliable and maintainable applications.
In software development, unit testing ensures that the smallest parts of an application (method, class, or function) work correctly in isolation. In C#, the most commonly used testing frameworks are xUnit, NUnit, and MSTest. Unit tests help detect errors early, improve code quality, and make safe refactoring possible.
What is a Unit Test?
Unit tests verify whether a method produces the correct output for given inputs. Tests are usually isolated per scenario and decoupled from external dependencies (database, file system, network access).
// Example: Method to be tested
public class Calculator
{
public int Add(int a, int b) => a + b;
}
In the example above, the Add method is the unit to be tested.
Creating a Test Project
You can easily create a test project using Visual Studio or the .NET CLI:
// Create a test project using xUnit
dotnet new xunit -n Calculator.Tests
cd Calculator.Tests
// Add a reference to the main project
dotnet add reference ../Calculator/Calculator.csproj
Alternatively, you can use nunit or mstest templates:
dotnet new nunit -n ProjectName.Tests
dotnet new mstest -n ProjectName.Tests
Writing Tests with xUnit
xUnit is a modern and flexible testing framework widely used in the C# ecosystem.
The [Fact] attribute marks individual tests, while [Theory] represents parameterized tests.
using Xunit;
public class CalculatorTests
{
[Fact]
public void Add_ShouldReturnCorrectResult()
{
// Arrange
var calc = new Calculator();
// Act
int result = calc.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, 5, 4)]
[InlineData(10, -2, 8)]
public void Add_ShouldWorkWithDifferentValues(int a, int b, int expected)
{
var calc = new Calculator();
Assert.Equal(expected, calc.Add(a, b));
}
}
Fact: Tests a single scenario. Theory: Tests the same method with multiple data sets.
Writing Tests with NUnit
NUnit is a powerful and long-standing testing framework in the C# world. It’s known for its simplicity and extensive set of assertions.
using NUnit.Framework;
[TestFixture]
public class CalculatorTests
{
private Calculator calc;
[SetUp]
public void Setup()
{
calc = new Calculator();
}
[Test]
public void Add_Test()
{
int result = calc.Add(4, 6);
Assert.That(result, Is.EqualTo(10));
}
[TestCase(1, 2, 3)]
[TestCase(5, 5, 10)]
public void Add_WithDifferentValues_ShouldReturnCorrectResult(int a, int b, int expected)
{
Assert.That(calc.Add(a, b), Is.EqualTo(expected));
}
}
In NUnit, the [SetUp] method runs before each test, and [TestCase] is used for parameterized tests.
Writing Tests with MSTest
MSTest is Microsoft’s built-in testing framework that comes with Visual Studio. It’s often used in enterprise-level projects.
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class CalculatorTests
{
private Calculator calc;
[TestInitialize]
public void Init()
{
calc = new Calculator();
}
[TestMethod]
public void Add_Test()
{
int result = calc.Add(2, 8);
Assert.AreEqual(10, result);
}
[DataTestMethod]
[DataRow(1, 1, 2)]
[DataRow(-3, 5, 2)]
public void Add_WithDifferentValues_ShouldWorkProperly(int a, int b, int expected)
{
Assert.AreEqual(expected, calc.Add(a, b));
}
}
In MSTest, the [TestInitialize] attribute handles setup before each test, and [DataTestMethod] is used for data-driven testing.
Assert Methods
All testing frameworks include similar assertion structures:
Assert.Equal(expected, actual)→ Tests value equality.Assert.NotNull(obj)→ Verifies that an object is not null.Assert.True(condition)/Assert.False(condition)Assert.Throws<Exception>(() => ...)→ Tests for an expected exception.
// Example: Exception test
[Fact]
public void Divide_ShouldThrowDivideByZeroException()
{
var calc = new Calculator();
Assert.Throws<DivideByZeroException>(() => calc.Divide(5, 0));
}
Running Tests
You can run tests via the CLI or Visual Studio:
// Run tests using .NET CLI
dotnet test
In Visual Studio, you can use the “Test Explorer” window to run tests and view passed/failed results in a graphical interface.
Mocking and Dependency Isolation
Mocking is used to isolate real dependencies from the test environment. This ensures that tests only verify the target method and are not affected by external factors (e.g., databases, services).
using Moq;
using Xunit;
public interface IEmailService
{
void Send(string address, string message);
}
public class UserRegistration
{
private readonly IEmailService _mail;
public UserRegistration(IEmailService mail) => _mail = mail;
public void Register(string name)
{
_mail.Send("admin@site.com", $"{name} has been registered.");
}
}
public class UserRegistrationTests
{
[Fact]
public void Register_ShouldSendEmail()
{
var mock = new Mock<IEmailService>();
var reg = new UserRegistration(mock.Object);
reg.Register("John");
mock.Verify(m => m.Send("admin@site.com", "John has been registered."), Times.Once);
}
}
The Moq library creates mock objects to isolate the test environment.
Test Organization and Naming
- Test names should be descriptive:
MethodName_Scenario_ExpectedResult. - Each test should verify only one behavior.
- Follow the Arrange–Act–Assert structure.
- Keep test dependencies minimal.
- Tests should run independently from one another.
Real-World Example: Order Total Calculation Test
The following example tests a simple order total calculation.
// Application code
public class Order
{
public List<decimal> ProductPrices { get; set; } = new();
public decimal Total() => ProductPrices.Sum();
}
// Test
public class OrderTests
{
[Fact]
public void Total_ShouldReturnCorrectSum()
{
var order = new Order();
order.ProductPrices.AddRange(new[] { 10m, 20m, 30m });
Assert.Equal(60m, order.Total());
}
}
This example provides a basic unit test to ensure that business logic works correctly.
Performance and Best Practices
- Each test should be isolated and not depend on global state.
- Keep tests small – one test should validate one behavior.
- Use mocks or stubs to isolate external dependencies.
- Integrate tests into your Continuous Integration (CI) pipeline.
- Profile and optimize slow tests.
TL;DR
- Unit Test: Tests small, isolated pieces of code.
- xUnit: Modern, minimal, and widely used in the .NET community.
- NUnit: Classic framework with strong assertion and attribute-based structure.
- MSTest: Microsoft’s built-in solution, fully integrated with Visual Studio.
- Mocking: Isolates external dependencies using fake objects.
- AAA Model: Follow the Arrange–Act–Assert test pattern.
- The dotnet test command runs all tests from the CLI.
Related Articles
Dependency Injection Basics in C#
Learn the basics of Dependency Injection in C#, including managing dependencies, loose coupling, and improving testability.
Visual Studio / VS Code Tips for C#
Learn Visual Studio and VS Code tips for C# to improve productivity with shortcuts, extensions, and efficient workflows.