Ответ
Да, активно пишу модульные, интеграционные и 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));
}
}
Ключевые практики:
- Изоляция: Тесты не зависят от внешних сервисов (используются моки) и друг от друга.
- Паттерн AAA: Чёткое разделение на Arrange (подготовка), Act (действие), Assert (проверка).
- Смысловые имена: Имена тестовых методов следуют шаблону
MethodName_StateUnderTest_ExpectedBehavior. - CI/CD интеграция: Тесты запускаются автоматически в пайплайнах (GitHub Actions, Azure DevOps).
- Покрытие критических путей: Фокус на тестировании бизнес-логики, 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));
}
}
А по поводу практик, у меня в голове чётко:
- Изоляция — наше всё. Тест не должен зависеть ни от базы данных, которая упала, ни от интернета, который пропал, ни от соседнего теста. Всё замокал — и спокоен.
- Паттерн AAA (Arrange, Act, Assert) — святое. Не мешай подготовку, действие и проверки в одну кучу, а то потом сам не разберёшься.
- Имена тестов — чтобы было понятно, что сломалось. Не
Test1, аProcessOrder_ValidOrder_CallsRepository. Проснулся ночью — и сразу ясно, где искать. - В CI/CD это всё должно запускаться автоматом. Залил код — и поехали тесты, как на конвейере. Если упало — значит, где-то косяк, и надо разбираться, а не надеяться на "авось".
- И главное — не гнаться за каким-то ебучим процентом покрытия ради галочки. Важно покрыть тестами ключевую бизнес-логику, обработку ошибок и эти вот все пограничные случаи, где всё обычно и ломается. А геттеры-сеттеры сами как-нибудь, ёпта.