xUnit / NUnit для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что это за библиотека и зачем нужна

xUnit и NUnit — это две самые популярные библиотеки для модульного тестирования (unit testing) в экосистеме .NET (C#, F#, VB.NET). Они позволяют разработчикам писать автоматические тесты, которые проверяют корректность работы отдельных модулей программы (классов, методов, функций).

Зачем это нужно?

  • Обнаружение ошибок на ранних этапах: Тесты запускаются после каждого изменения кода, что позволяет сразу выявить регрессии (когда новый код ломает старую функциональность).
  • Рефакторинг без страха: Вы можете смело переписывать код, если уверены, что тесты его покрывают. Если тесты проходят — всё работает как прежде.
  • Документация в действии: Тесты показывают, как именно должен использоваться тот или иной метод, какие параметры он принимает и что возвращает.
  • CI/CD интеграция: Тесты автоматически запускаются на серверах сборки (GitHub Actions, Azure DevOps, Jenkins) перед деплоем.

Разница между xUnit и NUnit:

  • NUnit — более старая библиотека (порт из Java JUnit), использует атрибуты [Test], [SetUp], [TearDown]. Имеет богатый набор атрибутов для параметризации.
  • xUnit — более современная библиотека, созданная автором NUnit. Использует атрибут [Fact] для обычных тестов и [Theory] для параметризованных. Отличается более строгим подходом к изоляции тестов (каждый тест — новый экземпляр класса).

В этой статье мы рассмотрим обе библиотеки, но основной фокус сделаем на xUnit, так как она рекомендуется Microsoft для новых проектов.

Установка

Установка производится через менеджер пакетов NuGet. Вы можете использовать консоль диспетчера пакетов (Package Manager Console) или .NET CLI.

Установка xUnit

// Через .NET CLI
dotnet add package xunit
dotnet add package xunit.runner.visualstudio

// Через Package Manager Console
Install-Package xunit
Install-Package xunit.runner.visualstudio

Установка NUnit

// Через .NET CLI
dotnet add package NUnit
dotnet add package NUnit3TestAdapter

// Через Package Manager Console
Install-Package NUnit
Install-Package NUnit3TestAdapter

Важно: Для запуска тестов в Visual Studio или Rider также требуется установить адаптер тестового раннера (например, xunit.runner.visualstudio или NUnit3TestAdapter).

Быстрый старт с примером кода

Предположим, у нас есть простой класс Calculator с методом Add. Напишем для него тесты в xUnit и NUnit.

Тестируемый класс (Calculator.cs)

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Divide(int a, int b)
    {
        if (b == 0)
            throw new DivideByZeroException("На ноль делить нельзя!");
        return a / b;
    }
}

Тест на xUnit (CalculatorTests.cs)

using Xunit;

public class CalculatorTests
{
    [Fact]
    public void Add_ShouldReturnSum_WhenTwoPositiveNumbers()
    {
        // Arrange (Подготовка)
        var calculator = new Calculator();
        int a = 5;
        int b = 3;

        // Act (Действие)
        int result = calculator.Add(a, b);

        // Assert (Проверка)
        Assert.Equal(8, result);
    }

    [Fact]
    public void Divide_ShouldThrowException_WhenDivideByZero()
    {
        var calculator = new Calculator();

        Assert.Throws<DivideByZeroException>(() => calculator.Divide(10, 0));
    }
}

Тест на NUnit (CalculatorTests.cs)

using NUnit.Framework;

[TestFixture]
public class CalculatorTests
{
    private Calculator _calculator;

    [SetUp]
    public void Setup()
    {
        _calculator = new Calculator();
    }

    [Test]
    public void Add_ShouldReturnSum_WhenTwoPositiveNumbers()
    {
        int result = _calculator.Add(5, 3);
        Assert.AreEqual(8, result);
    }

    [Test]
    public void Divide_ShouldThrowException_WhenDivideByZero()
    {
        Assert.Throws<DivideByZeroException>(() => _calculator.Divide(10, 0));
    }
}

Основные методы/функции (xUnit)

Рассмотрим ключевые методы и атрибуты xUnit, которые используются в 90% тестов.

1. [Fact]

Что делает: Помечает метод как обычный тест без параметров. Это самый простой и распространённый атрибут.

Параметры: Нет параметров. Можно использовать именованные параметры DisplayName (для отображения в Test Explorer).

[Fact(DisplayName = "Тест сложения двух чисел")]
public void Add_TwoNumbers_ReturnsSum()
{
    var calc = new Calculator();
    var result = calc.Add(2, 3);
    Assert.Equal(5, result);
}

2. [Theory]

Что делает: Помечает параметризованный тест. Вместе с ним используются атрибуты [InlineData], [MemberData] или [ClassData] для передачи данных.

Параметры: Атрибут сам по себе не принимает данные, но требует указания источника данных.

[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, -1, -2)]
[InlineData(0, 0, 0)]
public void Add_ShouldReturnCorrectSum(int a, int b, int expected)
{
    var calc = new Calculator();
    var result = calc.Add(a, b);
    Assert.Equal(expected, result);
}

3. Assert.Equal(expected, actual)

Что делает: Проверяет, что два значения равны. Это самый часто используемый метод Assert.

Параметры: expected (ожидаемое значение), actual (фактическое значение). Можно передать третий параметр — precision для чисел с плавающей точкой.

[Fact]
public void TestDoubleEquality()
{
    double result = 10.0 / 3.0;
    Assert.Equal(3.333, result, 2); // Проверка с точностью до 2 знаков
}

4. Assert.NotEqual(expected, actual)

Что делает: Проверяет, что два значения НЕ равны.

Параметры: Аналогично Assert.Equal.

[Fact]
public void Add_ShouldNotReturnWrongSum()
{
    var calc = new Calculator();
    var result = calc.Add(2, 2);
    Assert.NotEqual(5, result); // 2+2 != 5
}

5. Assert.Throws<TException>(Action)

Что делает: Проверяет, что код внутри делегата выбрасывает исключение указанного типа. Если исключение не выброшено — тест падает.

Параметры: Generic-параметр TException (тип исключения), делегат Action.

[Fact]
public void Divide_ByZero_ThrowsException()
{
    var calc = new Calculator();
    Assert.Throws<DivideByZeroException>(() => calc.Divide(10, 0));
}

6. Assert.True(condition) и Assert.False(condition)

Что делает: Проверяет, что булево условие истинно (True) или ложно (False).

Параметры: bool condition.

[Fact]
public void IsPositive_ShouldReturnTrue()
{
    int number = 5;
    Assert.True(number > 0);
    Assert.False(number < 0);
}

7. Assert.Null(object) и Assert.NotNull(object)

Что делает: Проверяет, является ли объект null (Null) или не null (NotNull).

Параметры: Объект для проверки.

[Fact]
public void GetUser_ShouldReturnNull_WhenUserNotFound()
{
    var repository = new UserRepository();
    var user = repository.GetById(999);
    Assert.Null(user);
}

8. Assert.Contains(expectedSubstring, actualString)

Что делает: Проверяет, содержит ли строка указанную подстроку. Также работает с коллекциями.

Параметры: expectedSubstring (искомая подстрока), actualString (строка для поиска).

[Fact]
public void GetGreeting_ShouldContainHello()
{
    var service = new GreetingService();
    var greeting = service.GetGreeting("Мир");
    Assert.Contains("Привет", greeting);
}

9. Assert.Collection(collection, elementInspectors...)

Что делает: Проверяет коллекцию, применяя к каждому элементу свой инспектор (проверку). Порядок важен.

Параметры: Коллекция и массив делегатов Action<T>.

[Fact]
public void GetNumbers_ShouldReturnCorrectSequence()
{
    var service = new NumberService();
    var numbers = service.GetEvenNumbers(6); // [2, 4, 6]

    Assert.Collection(numbers,
        item => Assert.Equal(2, item),
        item => Assert.Equal(4, item),
        item => Assert.Equal(6, item)
    );
}

10. Assert.All(collection, action)

Что делает: Применяет одну и ту же проверку ко всем элементам коллекции.

Параметры: Коллекция и делегат Action<T>.

[Fact]
public void AllNumbers_ShouldBePositive()
{
    var numbers = new List<int> { 1, 2, 3, 4, 5 };
    Assert.All(numbers, n => Assert.True(n > 0));
}

Реальный практический пример использования

Разработаем и протестируем простую систему управления заказами.

Класс OrderService

public class OrderService
{
    private readonly List<Order> _orders = new();

    public void AddOrder(Order order)
    {
        if (order == null)
            throw new ArgumentNullException(nameof(order));
        if (order.TotalAmount <= 0)
            throw new ArgumentException("Сумма заказа должна быть больше нуля");

        _orders.Add(order);
    }

    public decimal GetTotalRevenue()
    {
        return _orders.Sum(o => o.TotalAmount);
    }

    public Order GetOrderById(int id)
    {
        return _orders.FirstOrDefault(o => o.Id == id);
    }
}

public class Order
{
    public int Id { get; set; }
    public decimal TotalAmount { get; set; }
    public string CustomerName { get; set; }
}

Тесты для OrderService (xUnit)

using Xunit;

public class OrderServiceTests
{
    [Fact]
    public void AddOrder_ShouldIncreaseOrderCount()
    {
        // Arrange
        var service = new OrderService();
        var order = new Order { Id = 1, TotalAmount = 100, CustomerName = "Иван" };

        // Act
        service.AddOrder(order);

        // Assert
        var revenue = service.GetTotalRevenue();
        Assert.Equal(100, revenue);
    }

    [Fact]
    public void AddOrder_NullOrder_ThrowsArgumentNullException()
    {
        var service = new OrderService();
        Assert.Throws<ArgumentNullException>(() => service.AddOrder(null));
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-50)]
    public void AddOrder_InvalidAmount_ThrowsArgumentException                    

Рекомендаций