Писал ли тесты на C#?

Ответ

Да, активно пишу модульные, интеграционные и end-to-end тесты в экосистеме .NET. В работе использую следующий стек и подходы:

Стек технологий:

  • Фреймворки: xUnit (предпочтительно), NUnit, MSTest
  • Mocking: Moq, NSubstitute
  • Assertions: FluentAssertions для читаемых проверок
  • Интеграционное тестирование: Testcontainers для поднятия реальных БД в Docker, WebApplicationFactory для ASP.NET Core

Пример модульного теста с xUnit и Moq:

public class OrderServiceTests
{
    private readonly Mock<IRepository<Order>> _mockRepo;
    private readonly OrderService _sut; // System Under Test

    public OrderServiceTests()
    {
        _mockRepo = new Mock<IRepository<Order>>();
        _sut = new OrderService(_mockRepo.Object);
    }

    [Fact]
    public async Task ProcessOrder_ValidOrder_CallsRepositoryAndReturnsId()
    {
        // Arrange
        var order = new Order { Id = 0, Total = 100 };
        _mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
                 .Callback<Order>(o => o.Id = 123) // Симулируем присвоение ID
                 .Returns(Task.CompletedTask);

        // Act
        var resultId = await _sut.ProcessOrderAsync(order);

        // Assert
        resultId.Should().Be(123); // FluentAssertions
        _mockRepo.Verify(r => r.AddAsync(order), Times.Once);
        _mockRepo.VerifyNoOtherCalls();
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-10)]
    public void ValidateOrder_InvalidTotal_ThrowsException(decimal invalidTotal)
    {
        var order = new Order { Total = invalidTotal };
        Assert.Throws<ArgumentException>(() => _sut.ValidateOrder(order));
    }
}

Ключевые практики:

  1. Изоляция: Тесты не зависят от внешних сервисов (используются моки) и друг от друга.
  2. Паттерн AAA: Чёткое разделение на Arrange (подготовка), Act (действие), Assert (проверка).
  3. Смысловые имена: Имена тестовых методов следуют шаблону MethodName_StateUnderTest_ExpectedBehavior.
  4. CI/CD интеграция: Тесты запускаются автоматически в пайплайнах (GitHub Actions, Azure DevOps).
  5. Покрытие критических путей: Фокус на тестировании бизнес-логики, edge-кейсов и обработки ошибок, а не на достижении формального процента покрытия.

Ответ 18+ 🔞

А, ну ты про тесты спрашиваешь! Да, конечно, пишу, без этого сейчас никуда, как без рук. В .NET, само собой.

Слушай, вот мой обычный арсенал, которым пользуюсь, чтобы код не разъехался по швам, когда в него начинают тыкать палкой:

Чем работаю:

  • По фреймворкам: В основном xUnit — он мне как-то ближе, проще. Хотя NUnit и MSTest тоже встречал, конечно.
  • Чтобы подменять зависимости (эти все ваши репозитории и внешние апихи) — Moq. Иногда NSubstitute, если проект такой.
  • Чтобы проверки читались как книга, а не как шифровка из трёх букв — FluentAssertions. Ну, result.Should().Be(123) — это же понятнее, чем Assert.Equal(123, result), правда?
  • Для интеграционок, где надо базу поднять или сервис целиком — Testcontainers (это просто песня, когда в Docker'е постгресс запускается на время теста) и WebApplicationFactory.

Вот, смотри, как я обычно модулярину пишу, на примере какого-нибудь сервиса заказов:

public class OrderServiceTests
{
    private readonly Mock<IRepository<Order>> _mockRepo; // Подстава для репозитория
    private readonly OrderService _sut; // А это то, что мы собственно тестируем

    public OrderServiceTests()
    {
        _mockRepo = new Mock<IRepository<Order>>();
        _sut = new OrderService(_mockRepo.Object); // Подсовываем ему подставу
    }

    [Fact]
    public async Task ProcessOrder_ValidOrder_CallsRepositoryAndReturnsId()
    {
        // Arrange (Готовим)
        var order = new Order { Id = 0, Total = 100 };
        _mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
                 .Callback<Order>(o => o.Id = 123) // Говорим подставе: "Когда вызовут AddAsync, подставь айдишник 123"
                 .Returns(Task.CompletedTask);

        // Act (Дёргаем за ручку)
        var resultId = await _sut.ProcessOrderAsync(order);

        // Assert (Проверяем, что всё не сломалось)
        resultId.Should().Be(123); // FluentAssertions рулит
        _mockRepo.Verify(r => r.AddAsync(order), Times.Once); // Убеждаемся, что репозиторий дернули ровно один раз
        _mockRepo.VerifyNoOtherCalls(); // И больше него никто не трогал — чистая работа!
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-10)]
    public void ValidateOrder_InvalidTotal_ThrowsException(decimal invalidTotal)
    {
        // Тут проверяем, что на откровенную хуйню (нулевую или отрицательную сумму) сервис ругнётся как надо
        var order = new Order { Total = invalidTotal };
        Assert.Throws<ArgumentException>(() => _sut.ValidateOrder(order));
    }
}

А по поводу практик, у меня в голове чётко:

  1. Изоляция — наше всё. Тест не должен зависеть ни от базы данных, которая упала, ни от интернета, который пропал, ни от соседнего теста. Всё замокал — и спокоен.
  2. Паттерн AAA (Arrange, Act, Assert) — святое. Не мешай подготовку, действие и проверки в одну кучу, а то потом сам не разберёшься.
  3. Имена тестов — чтобы было понятно, что сломалось. Не Test1, а ProcessOrder_ValidOrder_CallsRepository. Проснулся ночью — и сразу ясно, где искать.
  4. В CI/CD это всё должно запускаться автоматом. Залил код — и поехали тесты, как на конвейере. Если упало — значит, где-то косяк, и надо разбираться, а не надеяться на "авось".
  5. И главное — не гнаться за каким-то ебучим процентом покрытия ради галочки. Важно покрыть тестами ключевую бизнес-логику, обработку ошибок и эти вот все пограничные случаи, где всё обычно и ломается. А геттеры-сеттеры сами как-нибудь, ёпта.