The Ultimate Guide to Writing NUnit Unit Tests in .NET 9

Overview

It is crucial for software developers to write effective unit tests in order to ensure that individual components of an application work correctly. By writing effective unit tests, code reliability and bug reduction are enhanced. This article explores how to write robust unit tests using NUnit in .NET 9 with code examples, patterns, and strategies as one of the most widely used unit testing frameworks for .NET applications.

Introduction to Unit Testing

Your application's unit tests verify the correctness of individual methods and functions by writing test cases for them. Before integrating a piece of code into a larger system, unit tests are used to ensure that each piece of code works in isolation. They are automated, repeatable, and serve as documentation for the behavior of a system.

What is NUnit?

.NET applications can be tested using NUnit, an open-source unit testing framework. NUnit provides attributes and assertions for testing various aspects of your code, such as expected outputs, handling exceptions, and mocking dependencies. It is easy to integrate NUnit into Visual Studio or other IDEs that are compatible with .NET.

Setting Up NUnit in .NET 9

 To write unit tests with NUnit in .NET 9, you must set up your development environment. Here's how:

Step 1. Create a .NET 9 Project

Using the .NET CLI or Visual Studio, you can create a new .NET project. We'll make a simple console application in this example:

dotnet new console -n NUnitExample

Step 2. Install NUnit and NUnit3TestAdapter

To run unit tests in Visual Studio, you need to install NUnit and the NUnit3TestAdapter. Use the following commands to install them:

dotnet add package NUnit

dotnet add package NUnit3TestAdapter

dotnet add package Microsoft.NET.Test.Sdk

Step 3. Create a Test Project

dotnet new nunit -n NUnitExample.Tests
cd NUnitExample.Tests

The next step is to create a new test project where you will write your unit tests:

dotnet add reference ../NUnitExample/NUnitExample.csproj

Basic NUnit Test Example

We will now write some simple unit tests to learn how NUnit works.

Testing a Simple Method

In the following example, we will add two integers using the method AddNumbers:

// In NUnitExample/Calculator.cs
namespace NUnitExample;
public class Calculator
{
    public int AddNumbers(int a, int b)
    {
        return checked(a + b);
    }
}

The next step is to write a unit test for this method.

// In NUnitExample.Tests/CalculatorTests.cs
 namespace NUnitExample.Tests;

[TestFixture]   // Identifyes this class as containing test methods
public class CalculatorTests
{
    private Calculator _calculator;

    [SetUp]  // Before each test, it runs
    public void SetUp()
    {
        _calculator = new Calculator();
    }

    [Test]  // This method is marked as a test
    public void AddNumbers_WhenGivenTwoIntegers_ReturnsCorrectSum()
    {
        // Arrange
        int a = 2;
        int b = 3;

        // Act
        int result = _calculator.AddNumbers(a, b);

        // Assert
        Assert.That(result, Is.EqualTo(5));
    }
}

Explanation of the Test

  • [TestFixture]: A test fixture, or unit test, is a class that contains unit tests.
  • [SetUp]: This attribute identifies the method that runs before each test. We initialize the Calculator object here.
  • [Test]: Assigning a unit test to a method using this attribute marks the method as a unit test.
  • Assert.AreEqual: An assertion that checks if the method's result matches the expected value is Assert.AreEqual.

Running the Test

Use the following command to run the tests:

dotnet test

Advanced NUnit Test Concepts


Testing Multiple Scenarios with TestCase

With NUnit's [TestCase] attribute, you can run a single test method with multiple sets of data.

  [TestCase(1, 2, 3)]
 [TestCase(10, 20, 30)]
 [TestCase(-1, -1, -2)]
 public void AddNumbers_WithVariousInputs_ReturnsCorrectSum(int a, int b, int expected)
 {
     int result = _calculator.AddNumbers(a, b);
     Assert.That(result, Is.EqualTo(expected));
 }

Parameterized Tests

Parameters can also be passed to your tests, making it easier to run the same test with different inputs:

[Test]
    public void AddNumbers_WithDifferentInputs_ReturnsExpectedSum([Values(1, 2, 3)] int a, [Values(4, 5, 6)] int b)
    {
        int result = _calculator.AddNumbers(a, b);
        Assert.That(result, Is.EqualTo(a + b));
    }

Writing Tests with Mocks

When unit testing, your methods may interact with external services or dependencies. Mocks simulate these dependencies.

Consider the following example of a service that fetches data from a database:

namespace NUnitExample;
public interface IDataService
{
    string GetData(int id);
}
namespace NUnitExample;
public class DataProcessor
{
    private readonly IDataService _dataService;

    public DataProcessor(IDataService dataService)
    {
        _dataService = dataService;
    }

    public string ProcessData(int id)
    {
        string data = _dataService.GetData(id);
        return $"Processed: {data}";
    }
}

Using IDataService as a mock, you can test DataProcessor: using Moq;  // Make sure to add Moq NuGet package

using Moq;
namespace NUnitExample.Tests;

public class DataProcessorTests
{
    [Test]
    public void ProcessData_WithMockedService_ReturnsProcessedData()
    {
        var mockService = new Mock<IDataService>();
        mockService.Setup(service => service.GetData(It.IsAny<int>())).Returns("Mocked Data");

        var processor = new DataProcessor(mockService.Object);
        string result = processor.ProcessData(1);

        Assert.That(result, Is.EqualTo("Processed: Mocked Data"));
    }
}

Explanation

  • Moq: The Moq mocking framework is a popular .NET mocking framework.
  • Mock<IDataService>: Creates a mock version of IDataService.
  • Setup(): The setup() method defines how the mocked method behaves.

Testing Exceptions and Edge Cases

To ensure robustness, unit tests should handle exceptions thrown by methods by using the

   [Test]
  public void AddNumbers_WithOverflow_ThrowsOverflowException()
  {
      // Arrange
      int maxInt = int.MaxValue;

      // Act & Assert
      var ex = Assert.Throws<OverflowException>(() => _calculator.AddNumbers(maxInt, 1));
      Assert.That(ex.Message, Is.EqualTo("Arithmetic operation resulted in an overflow."));
  }

Best Practices and Strategies

  1. Test Small Units of Code: Write tests for small, isolated units of work (for example, individual methods).
  2. Use Meaningful Names: Make sure your test methods have meaningful names that clearly explain what they are intended to do.
  3. Arrange-Act-Assert Pattern: The Arrange-Act-Assert pattern organizes your tests into three phases:
    • Arrange: Set up the test.
    • Act: Call the method under test.
    • Assert: Verify the result.
  4. Mock External Dependencies: Use mocks to simulate interactions with external services, databases, and APIs.
  5. Test Edge Cases: To ensure robustness, always test boundary conditions and edge cases.

Summary

Unit testing in .NET 9 with NUnit reduces the risk of bugs during development and ensures that your application behaves as expected. It is possible to write effective and reliable unit tests for your applications by following best practices, using advanced NUnit features like parameterized tests and mocks, and testing both typical and edge cases. 

The code examples for this article are available on my GitHub repository: GitHub - NUnitExample-UnitTesting.


Recommended Free Ebook
Similar Articles
Capgemini
Capgemini is a global leader in consulting, technology services, and digital transformation.